← All stacks
PY

Python

16 sections · 131 entries

Variables & Types

Variable Assignment

syntax
name = value
example
username = "alice"
age = 30
is_active = True
output
# alice, 30, True

Note Python variables are references to objects, not fixed memory boxes. No declaration keyword is needed.

Type Hints / Annotations

syntax
name: type = value
example
price: float = 19.99
items: list[str] = ["apple", "bread"]
user_map: dict[str, int] = {"alice": 1}
output
# No runtime enforcement — hints are for tools and readers

Note Type hints do not prevent wrong types at runtime. Use mypy or pyright for static checking. Since 3.12, you can use the new type statement for aliases.

Multiple Assignment

syntax
a, b, c = val1, val2, val3
example
x, y, z = 10, 20, 30
first, *rest = [1, 2, 3, 4, 5]
print(first, rest)
output
1 [2, 3, 4, 5]

Note The starred variable (*rest) captures remaining items as a list. Only one starred variable is allowed per assignment.

Type Checking at Runtime

syntax
type(obj)
isinstance(obj, ClassName)
example
score = 95.5
print(type(score))
print(isinstance(score, (int, float)))
output
<class 'float'>
True

Note Prefer isinstance() over type() == because isinstance handles subclasses correctly. The second arg can be a tuple of types.

None (Null Value)

syntax
variable = None
example
result = None
if result is None:
    print("No result yet")

# Common pattern: optional return
def find_user(user_id: int) -> str | None:
    return None
output
No result yet

Note Always compare to None with 'is' or 'is not', never == or !=. None is a singleton object.

Constants (Convention)

syntax
UPPER_SNAKE_CASE = value
example
MAX_RETRIES = 3
BASE_URL = "https://api.example.com"
PI = 3.14159

Note Python has no true constants. ALL_CAPS naming is a convention that signals 'do not reassign'. The typing.Final hint can be checked by type checkers.

Dynamic Typing & Reassignment

syntax
name = value1
name = value2  # different type is allowed
example
data = 42
print(type(data))
data = "now a string"
print(type(data))
output
<class 'int'>
<class 'str'>

Note Python is dynamically typed, so a variable can be rebound to any type. Type hints help catch accidental rebinding via static analysis.

Type Aliases (3.12+)

syntax
type AliasName = existing_type
example
type UserId = int
type Coordinate = tuple[float, float]
type UserMap = dict[UserId, str]

position: Coordinate = (40.71, -74.01)

Note The type statement (Python 3.12+) replaces TypeAlias from typing. It supports lazy evaluation and generic parameters.

Strings

f-strings (Formatted String Literals)

syntax
f"text {expression}"
example
name = "Alice"
balance = 1234.5
print(f"User: {name}")
print(f"Balance: ${balance:,.2f}")
print(f"{'centered':^20}")
output
User: Alice
Balance: $1,234.50
      centered      

Note Since Python 3.12, f-strings can contain backslashes and nested quotes freely. You can also nest f-strings inside f-strings.

Common String Methods

syntax
str.method()
example
email = "  [email protected]  "
print(email.strip().lower())
print("hello world".title())
print("python".startswith("py"))
print("2026-04-04".replace("-", "/"))
output
[email protected]
Hello World
True
2026/04/04

Note String methods always return new strings since strings are immutable. Chain methods for compact transformations.

String Slicing

syntax
string[start:stop:step]
example
word = "pythonic"
print(word[0:4])
print(word[-4:])
print(word[::2])
print(word[::-1])
output
pyth
onic
ptoi
cinohtyp

Note Slicing never raises IndexError, even if indices exceed the string length. Negative step reverses direction.

Multiline Strings

syntax
"""text
spanning lines"""
example
query = """
    SELECT name, email
    FROM users
    WHERE active = true
""".strip()
print(query)
output
SELECT name, email
    FROM users
    WHERE active = true

Note Triple-quoted strings preserve all whitespace and newlines. Use textwrap.dedent() or .strip() to control leading/trailing space.

Raw Strings

syntax
r"text with \backslashes"
example
path = r"C:\Users\alice\docs"
pattern = r"\d{3}-\d{4}"
print(path)
print(pattern)
output
C:\Users\alice\docs
\d{3}-\d{4}

Note Raw strings treat backslashes as literal characters. Essential for regex patterns and Windows paths. A raw string cannot end with an odd number of backslashes.

Encode & Decode

syntax
str.encode(encoding)
bytes.decode(encoding)
example
text = "caf\u00e9"
encoded = text.encode("utf-8")
print(encoded)
print(encoded.decode("utf-8"))
output
b'caf\xc3\xa9'
caf\u00e9

Note UTF-8 is the default encoding. Use errors='ignore' or errors='replace' to handle characters that cannot be encoded in the target encoding.

Join & Split

syntax
separator.join(iterable)
str.split(separator)
example
words = ["Python", "is", "great"]
sentence = " ".join(words)
print(sentence)

csv_row = "alice,30,engineer"
fields = csv_row.split(",")
print(fields)
output
Python is great
['alice', '30', 'engineer']

Note split() with no arguments splits on any whitespace and removes empty strings. split(',') keeps empty strings between consecutive delimiters.

String Content Checks

syntax
str.isdigit() / str.isalpha() / str.isalnum()
example
pin = "4821"
print(pin.isdigit())
print("hello123".isalnum())
print("  ".isspace())
print("api" in "api_key_value")
output
True
True
True
True

Note The 'in' operator checks for substring presence and is usually more practical than the .is*() family for real-world validation.

Numbers

Integers & Floats

syntax
x = 42      # int
y = 3.14    # float
example
count = 1_000_000
ratio = 0.618
print(type(count), type(ratio))
print(count + ratio)
output
<class 'int'> <class 'float'>
1000000.618

Note Underscores in numeric literals are ignored and serve as visual separators. Python ints have unlimited precision.

Arithmetic Operators

syntax
+ - * / // % **
example
print(17 / 5)
print(17 // 5)
print(17 % 5)
print(2 ** 10)
output
3.4
3
2
1024

Note / always returns a float. // is floor division (rounds toward negative infinity, not toward zero). So -7 // 2 gives -4, not -3.

abs() and round()

syntax
abs(number)
round(number, ndigits)
example
print(abs(-42.5))
print(round(3.14159, 2))
print(round(2.5))
print(round(3.5))
output
42.5
3.14
2
4

Note round() uses banker's rounding — it rounds to the nearest even number when the value is exactly halfway. This surprises many developers.

math Module Essentials

syntax
import math
example
import math
print(math.ceil(4.2))
print(math.floor(4.8))
print(math.sqrt(144))
print(math.log(100, 10))
print(math.pi)
output
5
4
12.0
2.0
3.141592653589793

Note math functions work on ints and floats but not on complex numbers. Use cmath for complex math operations.

Complex Numbers

syntax
z = real + imagj
example
z = 3 + 4j
print(z.real, z.imag)
print(abs(z))
output
3.0 4.0
5.0

Note abs() on a complex number returns its magnitude. Use the cmath module for complex-specific functions like phase and polar conversion.

Numeric Type Conversion

syntax
int(x)
float(x)
complex(real, imag)
example
print(int("42"))
print(int(9.99))
print(float("3.14"))
print(int("0xff", 16))
output
42
9
3.14
255

Note int() truncates toward zero (not floor). int('3.14') raises ValueError; convert to float first, then to int.

Decimal for Precision

syntax
from decimal import Decimal
example
from decimal import Decimal

# Floating point issue
print(0.1 + 0.2)

# Decimal precision
result = Decimal("0.1") + Decimal("0.2")
print(result)
output
0.30000000000000004
0.3

Note Always pass strings to Decimal(), not floats. Decimal(0.1) inherits the float imprecision. Critical for financial calculations.

Number Formatting

syntax
f"{value:format_spec}"
example
price = 1234567.891
print(f"{price:,.2f}")
print(f"{0.856:.1%}")
print(f"{42:08b}")
print(f"{255:#06x}")
output
1,234,567.89
85.6%
00101010
0x00ff

Note Format spec mini-language: comma for grouping, .Nf for decimal places, % for percent, b/o/x for binary/octal/hex.

Lists

Creating Lists

syntax
items = [val1, val2, ...]
items = list(iterable)
example
colors = ["red", "green", "blue"]
numbers = list(range(1, 6))
mixed = [1, "two", 3.0, True]
empty = []
print(numbers)
output
[1, 2, 3, 4, 5]

Note Lists can hold any mix of types. Use list() to convert other iterables (tuples, sets, generators) into lists.

Indexing & Negative Indexing

syntax
items[index]
items[-index]
example
fruits = ["apple", "banana", "cherry", "date"]
print(fruits[0])
print(fruits[-1])
print(fruits[-2])
output
apple
date
cherry

Note Index 0 is the first element, -1 is the last. Accessing an index beyond the list length raises IndexError.

List Slicing

syntax
items[start:stop:step]
example
nums = [10, 20, 30, 40, 50, 60]
print(nums[1:4])
print(nums[::2])
print(nums[::-1])
nums[1:3] = [200, 300]
print(nums)
output
[20, 30, 40]
[10, 30, 50]
[60, 50, 40, 30, 20, 10]
[10, 200, 300, 40, 50, 60]

Note Slice assignment can replace a range of elements. The replacement does not need to be the same length as the slice being replaced.

append, extend, insert

syntax
list.append(item)
list.extend(iterable)
list.insert(index, item)
example
tasks = ["email"]
tasks.append("report")
tasks.insert(0, "standup")
tasks.extend(["review", "deploy"])
print(tasks)
output
['standup', 'email', 'report', 'review', 'deploy']

Note append adds one item; extend adds each element from an iterable. Using append with a list nests it: [1].append([2,3]) gives [1, [2,3]].

remove, pop, del, clear

syntax
list.remove(value)
list.pop(index)
del list[index]
example
items = ["a", "b", "c", "d", "e"]
items.remove("c")
print(items)
last = items.pop()
print(last, items)
del items[0]
print(items)
output
['a', 'b', 'd', 'e']
e ['a', 'b', 'd']
['b', 'd']

Note remove() deletes the first matching value (raises ValueError if missing). pop() removes by index and returns the value. del removes by index without returning.

Sorting & Reversing

syntax
list.sort(key=None, reverse=False)
sorted(iterable, key=None, reverse=False)
example
prices = [29.99, 5.50, 12.00, 89.99]
prices.sort()
print(prices)

names = ["Charlie", "alice", "Bob"]
ordered = sorted(names, key=str.lower)
print(ordered)
output
[5.5, 12.0, 29.99, 89.99]
['alice', 'Bob', 'Charlie']

Note .sort() modifies the list in place and returns None. sorted() returns a new list. Use key= for custom ordering logic.

List Comprehensions

syntax
[expression for item in iterable if condition]
example
prices = [10, 25, 50, 75, 100]
affordable = [p for p in prices if p <= 50]
print(affordable)

squares = [n ** 2 for n in range(1, 6)]
print(squares)
output
[10, 25, 50]
[1, 4, 9, 16, 25]

Note Comprehensions are more Pythonic and faster than equivalent for-loop-with-append patterns. Keep them simple; if nesting gets deep, use a regular loop.

List Unpacking & Starred Expressions

syntax
a, b, *rest = some_list
example
coords = [10, 20, 30, 40, 50]
x, y, *remaining = coords
print(x, y, remaining)

first, *_, last = coords
print(first, last)
output
10 20 [30, 40, 50]
10 50

Note Use _ as a throwaway variable name for values you do not need. The starred variable always produces a list, even if empty.

Useful List Operations

syntax
len() / in / enumerate() / zip()
example
items = ["pen", "notebook", "eraser"]
print(len(items))
print("pen" in items)

for idx, item in enumerate(items, start=1):
    print(f"{idx}. {item}")
output
3
True
1. pen
2. notebook
3. eraser

Note enumerate() gives (index, value) pairs. Pass start= to begin counting from a number other than 0.

Dictionaries

Creating Dictionaries

syntax
d = {key: value, ...}
d = dict(key=value, ...)
example
user = {"name": "Alice", "age": 30, "active": True}
from_pairs = dict(host="localhost", port=8080)
print(user)
print(from_pairs)
output
{'name': 'Alice', 'age': 30, 'active': True}
{'host': 'localhost', 'port': 8080}

Note Keys must be hashable (strings, numbers, tuples of hashables). Lists and dicts cannot be keys.

Accessing & Modifying Values

syntax
d[key]
d[key] = value
example
config = {"debug": False, "port": 3000}
print(config["port"])
config["debug"] = True
config["host"] = "0.0.0.0"
print(config)
output
3000
{'debug': True, 'port': 3000, 'host': '0.0.0.0'}

Note Accessing a missing key with d[key] raises KeyError. Use d.get(key) to safely return None instead.

get() with Default Value

syntax
d.get(key, default)
example
settings = {"theme": "dark"}
theme = settings.get("theme", "light")
lang = settings.get("language", "en")
print(theme, lang)
output
dark en

Note get() returns the default when the key is missing but does NOT add it to the dict. Use setdefault() if you want to also store the default.

Dictionary Methods

syntax
d.keys() / d.values() / d.items()
example
scores = {"alice": 95, "bob": 82, "carol": 91}
print(list(scores.keys()))
print(list(scores.values()))

for name, score in scores.items():
    print(f"{name}: {score}")
output
['alice', 'bob', 'carol']
[95, 82, 91]
alice: 95
bob: 82
carol: 91

Note keys(), values(), and items() return view objects that reflect changes to the dict in real time.

Dictionary Comprehensions

syntax
{key_expr: val_expr for item in iterable if condition}
example
words = ["hello", "world", "python"]
lengths = {w: len(w) for w in words}
print(lengths)

original = {"a": 1, "b": 2, "c": 3}
filtered = {k: v for k, v in original.items() if v >= 2}
print(filtered)
output
{'hello': 5, 'world': 5, 'python': 6}
{'b': 2, 'c': 3}

Note Dict comprehensions are great for transforming or filtering dictionaries in a single expression.

Merging Dictionaries (| Operator)

syntax
merged = d1 | d2
d1 |= d2  # in-place
example
defaults = {"color": "blue", "size": "medium"}
overrides = {"size": "large", "bold": True}
final = defaults | overrides
print(final)
output
{'color': 'blue', 'size': 'large', 'bold': True}

Note The | operator (Python 3.9+) creates a new dict. Right-hand side wins on duplicate keys. Use |= to merge in place.

defaultdict

syntax
from collections import defaultdict
dd = defaultdict(factory)
example
from collections import defaultdict

word_count = defaultdict(int)
for word in "the cat sat on the mat".split():
    word_count[word] += 1
print(dict(word_count))
output
{'the': 2, 'cat': 1, 'sat': 1, 'on': 1, 'mat': 1}

Note defaultdict auto-creates missing keys using the factory function. Common factories: int (0), list ([]), set (set()).

Counter

syntax
from collections import Counter
example
from collections import Counter

letters = Counter("mississippi")
print(letters.most_common(3))

inventory = Counter(apples=5, oranges=3)
inventory.update(apples=2)
print(inventory["apples"])
output
[('s', 4), ('i', 4), ('p', 2)]
7

Note Counter supports arithmetic: Counter('aab') - Counter('ab') gives Counter({'a': 1}). most_common() returns elements in descending frequency.

Tuples & Sets

Creating & Using Tuples

syntax
t = (val1, val2, ...)
t = val1, val2
example
point = (10, 20)
singleton = (42,)
x, y = point
print(f"x={x}, y={y}")
print(len(singleton))
output
x=10, y=20
1

Note A single-element tuple requires a trailing comma: (42,). Without it, (42) is just an integer in parentheses. Tuples are immutable.

Named Tuples

syntax
from collections import namedtuple
Point = namedtuple('Point', ['x', 'y'])
example
from collections import namedtuple

Color = namedtuple("Color", ["red", "green", "blue"])
sky = Color(135, 206, 235)
print(sky.red, sky.blue)
print(sky._asdict())
output
135 235
{'red': 135, 'green': 206, 'blue': 235}

Note Named tuples give readable field access while staying immutable. For mutable fields or defaults, consider dataclasses instead.

NamedTuple (typing-based)

syntax
from typing import NamedTuple
class Name(NamedTuple):
    field: type
example
from typing import NamedTuple

class Endpoint(NamedTuple):
    host: str
    port: int = 443

api = Endpoint("api.example.com")
print(api.host, api.port)
output
api.example.com 443

Note The class-based NamedTuple supports type annotations and default values, making it more modern than the functional namedtuple() form.

Creating Sets

syntax
s = {val1, val2, ...}
s = set(iterable)
example
tags = {"python", "tutorial", "beginner"}
from_list = set([1, 2, 2, 3, 3, 3])
print(from_list)

empty_set = set()
print(type(empty_set))
output
{1, 2, 3}
<class 'set'>

Note Use set() for an empty set, NOT {}. Empty braces {} create an empty dictionary, not a set.

Set Operations

syntax
a | b  (union)
a & b  (intersection)
a - b  (difference)
a ^ b  (symmetric difference)
example
frontend = {"alice", "bob", "carol"}
backend = {"bob", "carol", "dave"}

print(frontend | backend)
print(frontend & backend)
print(frontend - backend)
print(frontend ^ backend)
output
{'alice', 'bob', 'carol', 'dave'}
{'bob', 'carol'}
{'alice'}
{'alice', 'dave'}

Note Set operations are extremely fast (O(min(len(a), len(b))) for intersection). Use them instead of nested loops for membership checks.

frozenset (Immutable Set)

syntax
fs = frozenset(iterable)
example
permissions = frozenset(["read", "write"])
print("read" in permissions)

# Can be used as a dictionary key
cache = {permissions: "admin"}
print(cache[frozenset(["read", "write"])])
output
True
admin

Note frozenset is hashable, so it can be used as a dict key or as an element inside another set. Regular sets cannot.

Set Mutation Methods

syntax
s.add(elem)
s.discard(elem)
s.remove(elem)
example
active = {"alice", "bob"}
active.add("carol")
active.discard("bob")
active.discard("nonexistent")
print(active)
output
{'alice', 'carol'}

Note discard() silently ignores missing elements. remove() raises KeyError if the element is not found. Prefer discard() when absence is acceptable.

Control Flow

if / elif / else

syntax
if condition:
    ...
elif condition:
    ...
else:
    ...
example
score = 85
if score >= 90:
    grade = "A"
elif score >= 80:
    grade = "B"
elif score >= 70:
    grade = "C"
else:
    grade = "F"
print(grade)
output
B

Note Python uses indentation instead of braces. There is no switch statement in older Python; use match/case (3.10+) for pattern matching.

Conditional (Ternary) Expression

syntax
value_if_true if condition else value_if_false
example
age = 20
status = "adult" if age >= 18 else "minor"
print(status)

# Nested (use sparingly)
label = "high" if age > 60 else "mid" if age > 30 else "young"
print(label)
output
adult
young

Note Nested ternaries hurt readability fast. If you need more than one level, use a regular if/elif chain instead.

match / case (Structural Pattern Matching)

syntax
match subject:
    case pattern:
        ...
example
command = {"action": "move", "x": 10, "y": 20}

match command:
    case {"action": "move", "x": x, "y": y}:
        print(f"Moving to ({x}, {y})")
    case {"action": "stop"}:
        print("Stopping")
    case _:
        print("Unknown command")
output
Moving to (10, 20)

Note match/case (Python 3.10+) is structural pattern matching, not just a switch. It can destructure dicts, sequences, and objects in the pattern.

for Loops

syntax
for item in iterable:
    ...
example
total = 0
prices = [12.50, 8.99, 24.00]
for price in prices:
    total += price
print(f"Total: ${total:.2f}")

for i in range(3):
    print(f"Attempt {i + 1}")
output
Total: $45.49
Attempt 1
Attempt 2
Attempt 3

Note range(n) produces 0 through n-1. Use range(start, stop, step) for more control. Python for loops iterate over any iterable, not just numeric ranges.

while Loops

syntax
while condition:
    ...
example
attempts = 0
max_attempts = 3

while attempts < max_attempts:
    attempts += 1
    print(f"Try {attempts}")

print("Done")
output
Try 1
Try 2
Try 3
Done

Note Make sure the condition eventually becomes False; otherwise you get an infinite loop. Use Ctrl+C to interrupt a runaway loop.

break & continue

syntax
break    # exit loop
continue # skip to next iteration
example
for n in range(1, 20):
    if n % 7 == 0:
        print(f"Found first multiple of 7: {n}")
        break
    if n % 2 == 0:
        continue
    print(f"Odd: {n}")
output
Odd: 1
Odd: 3
Odd: 5
Found first multiple of 7: 7

Note break exits only the innermost loop. To break out of nested loops, refactor into a function and use return.

else Clause on Loops

syntax
for item in iterable:
    ...
else:
    # runs if loop completed without break
example
targets = [2, 4, 6, 8]
for num in targets:
    if num % 5 == 0:
        print(f"Found multiple of 5: {num}")
        break
else:
    print("No multiple of 5 found")
output
No multiple of 5 found

Note The else block runs only if the loop was NOT terminated by break. This is one of Python's most misunderstood features. Think of it as 'no-break'.

Walrus Operator (:=)

syntax
if (name := expression):
example
data = [5, 12, 3, 18, 7, 22]
high = [x for x in data if (doubled := x * 2) > 20]
print(high)

import re
text = "Order #12345 confirmed"
if (match := re.search(r"#(\d+)", text)):
    print(f"Order ID: {match.group(1)}")
output
[12, 18, 22]
Order ID: 12345

Note The walrus operator (Python 3.8+) assigns and returns a value in one step. Most useful in while-loops, if-statements, and comprehensions to avoid repeated computation.

Functions

Defining Functions

syntax
def name(params):
    """docstring"""
    return value
example
def greet(name: str, greeting: str = "Hello") -> str:
    """Build a personalized greeting."""
    return f"{greeting}, {name}!"

print(greet("Alice"))
print(greet("Bob", greeting="Hey"))
output
Hello, Alice!
Hey, Bob!

Note Functions without an explicit return statement return None. Docstrings are accessible via help(func) and func.__doc__.

*args and **kwargs

syntax
def func(*args, **kwargs):
example
def log_event(event: str, *tags: str, **metadata: str):
    print(f"Event: {event}")
    print(f"Tags: {tags}")
    print(f"Meta: {metadata}")

log_event("login", "auth", "user", ip="10.0.0.1")
output
Event: login
Tags: ('auth', 'user')
Meta: {'ip': '10.0.0.1'}

Note *args collects extra positional arguments as a tuple, **kwargs collects extra keyword arguments as a dict. They can coexist in the same signature.

Keyword-Only & Positional-Only Args

syntax
def func(pos_only, /, normal, *, kw_only):
example
def fetch(url: str, /, *, timeout: int = 30, retries: int = 3):
    print(f"GET {url} (timeout={timeout}, retries={retries})")

fetch("https://api.example.com", timeout=10)
# fetch(url="...")  # TypeError: positional-only
output
GET https://api.example.com (timeout=10, retries=3)

Note / marks preceding params as positional-only. * marks following params as keyword-only. This prevents callers from relying on internal parameter names.

Lambda (Anonymous Functions)

syntax
lambda params: expression
example
square = lambda n: n ** 2
print(square(5))

users = [{"name": "Carol", "age": 28}, {"name": "Alice", "age": 35}]
users.sort(key=lambda u: u["age"])
print([u["name"] for u in users])
output
25
['Carol', 'Alice']

Note Lambdas are limited to a single expression. For anything with statements or multiple lines, use a regular def. Assigning a lambda to a variable is less readable than def.

Decorators

syntax
@decorator
def func(): ...
example
import functools
import time

def timer(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = func(*args, **kwargs)
        elapsed = time.perf_counter() - start
        print(f"{func.__name__} took {elapsed:.4f}s")
        return result
    return wrapper

@timer
def compute(n):
    return sum(range(n))

compute(1_000_000)
output
compute took 0.0XXXs

Note Always use @functools.wraps(func) in your wrapper so that the decorated function preserves its name and docstring. Decorators run at import time.

Closures

syntax
def outer():
    captured = value
    def inner():
        return captured
    return inner
example
def make_multiplier(factor: int):
    def multiply(n: int) -> int:
        return n * factor
    return multiply

double = make_multiplier(2)
triple = make_multiplier(3)
print(double(10), triple(10))
output
20 30

Note The inner function captures the enclosing variable by reference, not by value. See Common Mistakes for the late-binding closure gotcha.

Generators with yield

syntax
def gen():
    yield value
example
def countdown(n: int):
    while n > 0:
        yield n
        n -= 1

for tick in countdown(3):
    print(tick)

# Generators are lazy — values produced one at a time
nums = countdown(1_000_000)
print(next(nums))
output
3
2
1
1000000

Note Generators produce values lazily, consuming almost no memory regardless of size. They are single-use: once exhausted, they cannot be restarted.

Type Hints for Functions

syntax
def func(param: Type) -> ReturnType:
example
from collections.abc import Callable

def apply(
    values: list[int],
    transform: Callable[[int], int]
) -> list[int]:
    return [transform(v) for v in values]

result = apply([1, 2, 3], lambda x: x * 10)
print(result)
output
[10, 20, 30]

Note Use collections.abc.Callable for function type hints. The format is Callable[[ArgTypes], ReturnType]. For complex signatures, consider typing.Protocol.

Recursion

syntax
def func(n):
    if base_case: return value
    return func(modified_n)
example
def factorial(n: int) -> int:
    if n <= 1:
        return 1
    return n * factorial(n - 1)

print(factorial(6))

import sys
print(f"Recursion limit: {sys.getrecursionlimit()}")
output
720
Recursion limit: 1000

Note Python's default recursion limit is 1000. Deeply recursive algorithms should be rewritten iteratively or use sys.setrecursionlimit() with caution.

Classes & OOP

Defining a Class

syntax
class Name:
    def __init__(self, ...):
        self.attr = value
example
class BankAccount:
    def __init__(self, owner: str, balance: float = 0):
        self.owner = owner
        self.balance = balance

    def deposit(self, amount: float) -> None:
        self.balance += amount

acct = BankAccount("Alice", 100)
acct.deposit(50)
print(f"{acct.owner}: ${acct.balance}")
output
Alice: $150

Note self is not a keyword; it is a convention for the first parameter of instance methods. Python passes the instance automatically.

Inheritance & super()

syntax
class Child(Parent):
    def __init__(self):
        super().__init__()
example
class Vehicle:
    def __init__(self, make: str, year: int):
        self.make = make
        self.year = year

class ElectricCar(Vehicle):
    def __init__(self, make: str, year: int, range_km: int):
        super().__init__(make, year)
        self.range_km = range_km

    def __repr__(self) -> str:
        return f"{self.year} {self.make} ({self.range_km}km range)"

car = ElectricCar("Tesla", 2026, 600)
print(car)
output
2026 Tesla (600km range)

Note Always call super().__init__() in the child to ensure the parent is properly initialized. Python supports multiple inheritance via MRO (Method Resolution Order).

@property (Getters & Setters)

syntax
@property
def attr(self): ...
@attr.setter
def attr(self, value): ...
example
class Temperature:
    def __init__(self, celsius: float):
        self._celsius = celsius

    @property
    def fahrenheit(self) -> float:
        return self._celsius * 9 / 5 + 32

    @property
    def celsius(self) -> float:
        return self._celsius

    @celsius.setter
    def celsius(self, value: float) -> None:
        if value < -273.15:
            raise ValueError("Below absolute zero")
        self._celsius = value

t = Temperature(100)
print(t.fahrenheit)
t.celsius = 0
print(t.fahrenheit)
output
212.0
32.0

Note @property lets you expose computed values as attributes and add validation to setters without changing the caller's syntax.

@classmethod & @staticmethod

syntax
@classmethod
def method(cls, ...): ...
@staticmethod
def method(...): ...
example
class DateRecord:
    def __init__(self, year: int, month: int, day: int):
        self.year = year
        self.month = month
        self.day = day

    @classmethod
    def from_string(cls, date_str: str) -> "DateRecord":
        y, m, d = map(int, date_str.split("-"))
        return cls(y, m, d)

    @staticmethod
    def is_valid_year(year: int) -> bool:
        return 1 <= year <= 9999

record = DateRecord.from_string("2026-04-04")
print(record.year, DateRecord.is_valid_year(2026))
output
2026 True

Note @classmethod receives the class as first arg (cls) and is commonly used for alternative constructors. @staticmethod receives neither self nor cls.

Dataclasses

syntax
from dataclasses import dataclass
@dataclass
class Name:
    field: type
example
from dataclasses import dataclass, field

@dataclass
class Product:
    name: str
    price: float
    tags: list[str] = field(default_factory=list)

    @property
    def display_price(self) -> str:
        return f"${self.price:.2f}"

p = Product("Widget", 9.99, ["sale"])
print(p)
print(p.display_price)
output
Product(name='Widget', price=9.99, tags=['sale'])
$9.99

Note Dataclasses auto-generate __init__, __repr__, and __eq__. Use field(default_factory=list) for mutable defaults, never a bare [] as a default.

__slots__ (Memory Optimization)

syntax
class Name:
    __slots__ = ('attr1', 'attr2')
example
@dataclass(slots=True)
class Point:
    x: float
    y: float

p = Point(1.0, 2.0)
print(p)
# p.z = 3.0  # AttributeError: no __dict__
output
Point(x=1.0, y=2.0)

Note __slots__ prevents dynamic attribute creation and reduces memory by eliminating the per-instance __dict__. Python 3.10+ dataclasses support slots=True directly.

Abstract Base Classes

syntax
from abc import ABC, abstractmethod
class Base(ABC):
    @abstractmethod
    def method(self): ...
example
from abc import ABC, abstractmethod

class Shape(ABC):
    @abstractmethod
    def area(self) -> float: ...

class Circle(Shape):
    def __init__(self, radius: float):
        self.radius = radius

    def area(self) -> float:
        return 3.14159 * self.radius ** 2

# shape = Shape()  # TypeError: can't instantiate abstract class
circle = Circle(5)
print(f"Area: {circle.area():.2f}")
output
Area: 78.54

Note Abstract classes cannot be instantiated directly. Subclasses must implement all @abstractmethod methods or they will also be abstract.

Protocols (Structural Typing)

syntax
from typing import Protocol
class Drawable(Protocol):
    def draw(self) -> None: ...
example
from typing import Protocol

class Saveable(Protocol):
    def save(self, path: str) -> None: ...

class Document:
    def save(self, path: str) -> None:
        print(f"Saved to {path}")

def backup(item: Saveable, dest: str) -> None:
    item.save(dest)

backup(Document(), "/tmp/doc.txt")
output
Saved to /tmp/doc.txt

Note Protocols enable duck-typing with static type checking. Classes do not need to explicitly inherit from the Protocol; they just need matching methods.

Common Dunder Methods

syntax
__str__, __repr__, __len__, __eq__, __lt__, __hash__
example
class Money:
    def __init__(self, amount: float, currency: str = "USD"):
        self.amount = amount
        self.currency = currency

    def __repr__(self) -> str:
        return f"Money({self.amount}, '{self.currency}')"

    def __str__(self) -> str:
        return f"{self.currency} {self.amount:.2f}"

    def __eq__(self, other: object) -> bool:
        if not isinstance(other, Money):
            return NotImplemented
        return self.amount == other.amount and self.currency == other.currency

print(Money(42.50))
print(repr(Money(42.50)))
output
USD 42.50
Money(42.5, 'USD')

Note __repr__ should produce an unambiguous developer-facing string. __str__ is the user-facing version. Return NotImplemented (not raise) from __eq__ for unknown types.

File I/O

Reading Files

syntax
with open(path, 'r') as f:
    content = f.read()
example
with open("config.txt", "r", encoding="utf-8") as f:
    content = f.read()
    print(content[:50])

# Read line by line (memory efficient)
with open("data.log") as f:
    for line in f:
        print(line.strip())

Note Always specify encoding='utf-8' explicitly. The default varies by platform. Iterating line-by-line avoids loading the entire file into memory.

Writing Files

syntax
with open(path, 'w') as f:
    f.write(text)
example
lines = ["alice,95", "bob,82", "carol,91"]

with open("scores.csv", "w", encoding="utf-8") as f:
    f.write("name,score\n")
    for line in lines:
        f.write(line + "\n")

# Append mode
with open("scores.csv", "a") as f:
    f.write("dave,88\n")

Note 'w' mode truncates the file first. Use 'a' to append. Use 'x' to create exclusively (fails if file exists).

pathlib (Modern Path Handling)

syntax
from pathlib import Path
example
from pathlib import Path

data_dir = Path("project") / "data"
data_dir.mkdir(parents=True, exist_ok=True)

config = data_dir / "settings.json"
config.write_text('{"debug": true}', encoding="utf-8")
print(config.read_text())
print(config.suffix, config.stem)
output
{"debug": true}
.json settings

Note pathlib is the modern replacement for os.path. The / operator joins paths. Use .resolve() for absolute paths, .exists() to check existence.

CSV Files

syntax
import csv
example
import csv

# Writing
with open("users.csv", "w", newline="", encoding="utf-8") as f:
    writer = csv.DictWriter(f, fieldnames=["name", "age"])
    writer.writeheader()
    writer.writerow({"name": "Alice", "age": 30})
    writer.writerow({"name": "Bob", "age": 25})

# Reading
with open("users.csv", newline="", encoding="utf-8") as f:
    for row in csv.DictReader(f):
        print(row["name"], row["age"])
output
Alice 30
Bob 25

Note Always pass newline='' when opening CSV files (the csv module handles line endings itself). DictReader/DictWriter are more readable than plain reader/writer.

JSON Files

syntax
import json
json.dump(obj, file)
obj = json.load(file)
example
import json
from pathlib import Path

config = {"host": "localhost", "port": 8080, "debug": True}

# Write
Path("config.json").write_text(
    json.dumps(config, indent=2), encoding="utf-8"
)

# Read
loaded = json.loads(Path("config.json").read_text())
print(loaded["port"])
output
8080

Note json.dump/load work with file objects. json.dumps/loads work with strings. Use indent= for readable output. Python dicts serialize directly to JSON objects.

Binary File I/O

syntax
with open(path, 'rb') as f:
    data = f.read()
example
# Copy a binary file
with open("image.png", "rb") as src:
    data = src.read()

with open("copy.png", "wb") as dst:
    dst.write(data)

print(f"Copied {len(data)} bytes")

Note Use 'rb' / 'wb' modes for binary files (images, archives, etc.). Never open binary files in text mode; it corrupts the data on some platforms.

Context Managers (with Statement)

syntax
with expression as variable:
    ...
example
from contextlib import contextmanager

@contextmanager
def temp_directory():
    import tempfile, shutil
    path = tempfile.mkdtemp()
    try:
        yield path
    finally:
        shutil.rmtree(path)

with temp_directory() as tmp:
    print(f"Working in {tmp}")

Note The with statement guarantees cleanup code runs even if an exception occurs. Use contextlib.contextmanager to build custom context managers from generators.

Finding Files with Glob

syntax
Path(dir).glob(pattern)
Path(dir).rglob(pattern)
example
from pathlib import Path

# All Python files in current directory
for py_file in Path(".").glob("*.py"):
    print(py_file.name)

# Recursive search
all_json = list(Path(".").rglob("*.json"))
print(f"Found {len(all_json)} JSON files")

Note glob() searches one level. rglob() searches recursively through all subdirectories. Both return Path objects.

Error Handling

try / except

syntax
try:
    ...
except ExceptionType as e:
    ...
example
try:
    value = int("not_a_number")
except ValueError as e:
    print(f"Conversion failed: {e}")

# Multiple exception types
try:
    result = {"a": 1}["b"]
except (KeyError, IndexError) as e:
    print(f"Lookup error: {e}")
output
Conversion failed: invalid literal for int() with base 10: 'not_a_number'
Lookup error: 'b'

Note Always catch specific exceptions. Bare 'except:' catches everything including SystemExit and KeyboardInterrupt, which is almost never what you want.

else & finally

syntax
try:
    ...
except:
    ...
else:
    ...
finally:
    ...
example
def safe_divide(a: float, b: float) -> float | None:
    try:
        result = a / b
    except ZeroDivisionError:
        print("Cannot divide by zero")
        return None
    else:
        print(f"Result: {result}")
        return result
    finally:
        print("Division attempted")

safe_divide(10, 3)
safe_divide(10, 0)
output
Result: 3.3333333333333335
Division attempted
Cannot divide by zero
Division attempted

Note else runs only when no exception occurred. finally always runs, even after return statements. Use else to keep the try block minimal.

Raising Exceptions

syntax
raise ExceptionType(message)
example
def withdraw(balance: float, amount: float) -> float:
    if amount <= 0:
        raise ValueError(f"Amount must be positive, got {amount}")
    if amount > balance:
        raise RuntimeError("Insufficient funds")
    return balance - amount

try:
    withdraw(100, 200)
except RuntimeError as e:
    print(e)
output
Insufficient funds

Note Use raise without arguments inside an except block to re-raise the current exception. Use 'raise NewError() from original' to chain exceptions.

Custom Exception Classes

syntax
class MyError(Exception):
    ...
example
class PaymentError(Exception):
    def __init__(self, amount: float, reason: str):
        self.amount = amount
        self.reason = reason
        super().__init__(f"Payment of ${amount:.2f} failed: {reason}")

try:
    raise PaymentError(49.99, "card declined")
except PaymentError as e:
    print(e)
    print(f"Amount: ${e.amount}")
output
Payment of $49.99 failed: card declined
Amount: $49.99

Note Custom exceptions should inherit from Exception (not BaseException). Add meaningful attributes so callers can inspect the error programmatically.

Exception Groups (3.11+)

syntax
raise ExceptionGroup(msg, [exc1, exc2])
except* ErrorType as eg:
example
def validate(data: dict) -> None:
    errors = []
    if not data.get("name"):
        errors.append(ValueError("name is required"))
    if not data.get("email"):
        errors.append(ValueError("email is required"))
    if errors:
        raise ExceptionGroup("Validation failed", errors)

try:
    validate({})
except* ValueError as eg:
    for err in eg.exceptions:
        print(f"  - {err}")
output
  - name is required
  - email is required

Note except* catches matching exceptions from the group while letting others propagate. ExceptionGroup is ideal for concurrent/batch operations that produce multiple errors.

Exception Chaining (from)

syntax
raise NewError() from original_error
example
class DatabaseError(Exception):
    pass

try:
    int("bad")
except ValueError as e:
    raise DatabaseError("Failed to parse DB record") from e
output
# Traceback shows both: ValueError → DatabaseError

Note Using 'from e' sets __cause__ so the traceback shows the chain. Use 'from None' to explicitly suppress the original exception context.

Common Built-in Exceptions

syntax
ValueError, TypeError, KeyError, IndexError, ...
example
# ValueError - wrong value for the type
# TypeError - wrong type entirely
# KeyError - missing dictionary key
# IndexError - list index out of range
# AttributeError - missing attribute
# FileNotFoundError - file doesn't exist
# PermissionError - insufficient permissions
# StopIteration - iterator exhausted

try:
    open("nonexistent.txt")
except FileNotFoundError:
    print("File not found")
output
File not found

Note Learn the hierarchy: FileNotFoundError is a subclass of OSError. Catching OSError also catches FileNotFoundError, PermissionError, etc.

Suppressing Exceptions

syntax
from contextlib import suppress
with suppress(ExceptionType):
    ...
example
from contextlib import suppress
import os

# Instead of try/except/pass
with suppress(FileNotFoundError):
    os.remove("temp_cache.dat")

print("Cleanup done")
output
Cleanup done

Note contextlib.suppress is cleaner than try/except/pass when you intentionally want to ignore specific exceptions. Do not suppress broad exception types.

Modules & Imports

Basic Imports

syntax
import module
from module import name
import module as alias
example
import json
from pathlib import Path
from collections import defaultdict, Counter
import numpy as np  # common alias convention

data = json.dumps({"key": "value"})
print(data)
output
{"key": "value"}

Note Convention: standard library imports first, then third-party, then local. Separate groups with a blank line. Use isort to auto-format.

__name__ == '__main__' Guard

syntax
if __name__ == '__main__':
    ...
example
# my_module.py
def compute_tax(price: float, rate: float = 0.08) -> float:
    return price * rate

if __name__ == "__main__":
    # Only runs when executed directly, not when imported
    result = compute_tax(100)
    print(f"Tax: ${result:.2f}")
output
Tax: $8.00

Note This guard prevents code from running when the module is imported by another file. Essential for reusable modules that also serve as scripts.

__all__ (Controlling Exports)

syntax
__all__ = ['name1', 'name2']
example
# utils.py
__all__ = ["format_price", "validate_email"]

def format_price(amount: float) -> str:
    return f"${amount:,.2f}"

def validate_email(email: str) -> bool:
    return "@" in email

def _internal_helper():  # not exported
    pass

Note __all__ defines what 'from module import *' exports. It does not prevent direct imports of unlisted names. Prefix internal helpers with underscore by convention.

Relative Imports

syntax
from . import sibling
from .. import parent_module
from .sibling import name
example
# project/
# ├── __init__.py
# ├── models.py
# └── services/
#     ├── __init__.py
#     └── auth.py

# Inside services/auth.py:
# from ..models import User
# from . import helpers

Note Relative imports only work inside packages (directories with __init__.py). They fail if you run the file directly as a script. Use -m flag: python -m package.module.

Virtual Environments

syntax
python -m venv env_name
source env_name/bin/activate
example
# Create a virtual environment
# python -m venv .venv

# Activate (macOS/Linux)
# source .venv/bin/activate

# Activate (Windows)
# .venv\Scripts\activate

# Install packages
# pip install requests

# Save dependencies
# pip freeze > requirements.txt

# Deactivate
# deactivate

Note Always use virtual environments to isolate project dependencies. Python 3.12+ also has improved error messages when venv is not activated.

Package Structure

syntax
package/
  __init__.py
  module.py
  subpackage/
    __init__.py
example
# myapp/
# ├── __init__.py          # makes it a package
# ├── config.py
# ├── models/
# │   ├── __init__.py
# │   └── user.py
# └── utils/
#     ├── __init__.py
#     └── formatting.py

# In __init__.py, expose public API:
# from .config import Settings
# from .models.user import User

Note __init__.py runs when the package is imported. Use it to define the package's public interface. It can be empty for simple packages.

Dynamic Imports

syntax
import importlib
mod = importlib.import_module('name')
example
import importlib

# Import module by string name
json_mod = importlib.import_module("json")
result = json_mod.dumps({"dynamic": True})
print(result)

# Reload a module during development
# importlib.reload(my_module)
output
{"dynamic": true}

Note Dynamic imports are useful for plugin systems and lazy loading. importlib.reload() re-executes the module but existing references to old objects are not updated.

pip & Dependency Management

syntax
pip install package
pip install -r requirements.txt
example
# Install a package
# pip install requests

# Install specific version
# pip install requests==2.31.0

# Install from requirements file
# pip install -r requirements.txt

# Show installed packages
# pip list

# Show details about a package
# pip show requests

Note Consider using pip-tools, poetry, or uv for reproducible dependency management. pip freeze captures transitive deps, which can be fragile across platforms.

Comprehensions & Generators

List Comprehensions (Detailed)

syntax
[expr for x in iterable if cond]
example
# Transform + filter
temps_f = [32, 68, 95, 104, 50]
warm_c = [(f - 32) * 5 / 9 for f in temps_f if f > 60]
print([round(c, 1) for c in warm_c])

# Nested comprehension (flatten)
matrix = [[1, 2], [3, 4], [5, 6]]
flat = [n for row in matrix for n in row]
print(flat)
output
[20.0, 35.0, 40.0]
[1, 2, 3, 4, 5, 6]

Note In nested comprehensions, the outer loop comes first: [x for row in matrix for x in row]. This matches the order of equivalent nested for loops.

Dict Comprehensions (Detailed)

syntax
{key_expr: val_expr for item in iterable if cond}
example
# Invert a dictionary
http_codes = {200: "OK", 404: "Not Found", 500: "Server Error"}
code_lookup = {msg: code for code, msg in http_codes.items()}
print(code_lookup["Not Found"])

# From two parallel lists
keys = ["host", "port", "debug"]
vals = ["localhost", 8080, True]
config = {k: v for k, v in zip(keys, vals)}
print(config)
output
404
{'host': 'localhost', 'port': 8080, 'debug': True}

Note When inverting a dict, duplicate values in the original will cause key collisions; the last one wins.

Set Comprehensions

syntax
{expr for item in iterable if cond}
example
sentence = "the quick brown fox jumps over the lazy dog"
word_lengths = {len(word) for word in sentence.split()}
print(sorted(word_lengths))
output
[3, 4, 5]

Note Set comprehensions look like dict comprehensions but without the colon. They automatically deduplicate results.

Generator Expressions

syntax
(expr for item in iterable if cond)
example
# Sum of squares without building a list
total = sum(n ** 2 for n in range(1, 101))
print(total)

# Lazy evaluation
import sys
list_size = sys.getsizeof([x for x in range(10000)])
gen_size = sys.getsizeof(x for x in range(10000))
print(f"List: {list_size} bytes, Generator: {gen_size} bytes")
output
338350
List: ~85000 bytes, Generator: ~200 bytes

Note Generator expressions use parentheses instead of brackets and are lazy — they produce values on demand. When passed as the sole argument to a function, extra parens can be omitted.

yield from (Delegation)

syntax
yield from iterable
example
def flatten(nested):
    for item in nested:
        if isinstance(item, list):
            yield from flatten(item)
        else:
            yield item

data = [1, [2, [3, 4]], [5, 6]]
print(list(flatten(data)))
output
[1, 2, 3, 4, 5, 6]

Note yield from delegates to a sub-generator and passes values through. It handles send() and throw() transparently, unlike a manual for-loop with yield.

itertools Highlights

syntax
import itertools
example
from itertools import chain, islice, groupby, product

# Chain multiple iterables
all_items = list(chain([1, 2], [3, 4], [5]))
print(all_items)

# Take first N from any iterable
first_three = list(islice(range(1000000), 3))
print(first_three)

# Cartesian product
sizes = ["S", "M"]
colors = ["red", "blue"]
combos = list(product(sizes, colors))
print(combos)
output
[1, 2, 3, 4, 5]
[1, 2, 3]
[('S', 'red'), ('S', 'blue'), ('M', 'red'), ('M', 'blue')]

Note itertools functions return lazy iterators. They compose well and avoid creating intermediate lists. See also: accumulate, starmap, tee, zip_longest.

itertools.groupby

syntax
from itertools import groupby
for key, group in groupby(iterable, key=func):
example
from itertools import groupby

transactions = [
    {"type": "credit", "amount": 100},
    {"type": "credit", "amount": 200},
    {"type": "debit", "amount": 50},
    {"type": "debit", "amount": 75},
]

# Must be sorted by the grouping key first!
for ttype, group in groupby(transactions, key=lambda t: t["type"]):
    amounts = [t["amount"] for t in group]
    print(f"{ttype}: {amounts}")
output
credit: [100, 200]
debit: [50, 75]

Note groupby only groups consecutive items with the same key. Sort the data by the key first, or you get fragmented groups.

Walrus Operator in Comprehensions

syntax
[y := f(x) ... if y > threshold]
example
import math

values = [1, 10, 100, 1000, 10000]
# Compute once, use in both filter and output
result = [
    (v, log_v)
    for v in values
    if (log_v := math.log10(v)) >= 2
]
print(result)
output
[(100, 2.0), (1000, 3.0), (10000, 4.0)]

Note The walrus operator avoids computing the same expression twice in a comprehension. The variable leaks into the enclosing scope, which can be surprising.

Common Standard Library

datetime Module

syntax
from datetime import datetime, date, timedelta
example
from datetime import datetime, timedelta

now = datetime.now()
print(now.strftime("%Y-%m-%d %H:%M"))

deadline = now + timedelta(days=7, hours=3)
print(f"Due: {deadline:%B %d, %Y}")

parsed = datetime.strptime("2026-04-04", "%Y-%m-%d")
print(parsed.date())
output
2026-04-04 14:30
Due: April 11, 2026
2026-04-04

Note For timezone-aware datetimes, use datetime.now(tz=timezone.utc) instead of datetime.utcnow() which is naive and deprecated since 3.12.

collections Module

syntax
from collections import deque, OrderedDict, ChainMap
example
from collections import deque, ChainMap

# deque: efficient append/pop from both ends
buffer = deque(maxlen=3)
for n in range(5):
    buffer.append(n)
print(list(buffer))

# ChainMap: layered dict lookup
defaults = {"color": "blue", "size": 10}
overrides = {"color": "red"}
config = ChainMap(overrides, defaults)
print(config["color"], config["size"])
output
[2, 3, 4]
red 10

Note deque with maxlen auto-discards oldest items when full (ring buffer). ChainMap searches dicts in order, first match wins. Both are in collections.

functools Module

syntax
from functools import lru_cache, partial, reduce
example
from functools import lru_cache, partial

@lru_cache(maxsize=128)
def fibonacci(n: int) -> int:
    if n < 2:
        return n
    return fibonacci(n - 1) + fibonacci(n - 2)

print(fibonacci(30))
print(fibonacci.cache_info())

# partial: pre-fill arguments
def power(base, exp):
    return base ** exp

square = partial(power, exp=2)
print(square(7))
output
832040
CacheInfo(hits=28, misses=31, maxsize=128, currsize=31)
49

Note lru_cache requires hashable arguments. For unhashable args, consider cachetools or a manual cache. partial is great for adapting callback signatures.

re (Regular Expressions)

syntax
import re
re.search(pattern, string)
re.findall(pattern, string)
example
import re

text = "Contact: [email protected] or [email protected]"
emails = re.findall(r"[\w.]+@[\w.]+\.\w+", text)
print(emails)

# Named groups
log = "2026-04-04 ERROR: Connection timeout"
match = re.match(r"(?P<date>[\d-]+) (?P<level>\w+): (?P<msg>.+)", log)
if match:
    print(match.group("level"), match.group("msg"))
output
['[email protected]', '[email protected]']
ERROR Connection timeout

Note Always use raw strings (r'...') for regex patterns. For repeated use, compile with re.compile() for performance. Use re.VERBOSE for readable multi-line patterns.

typing Module Essentials

syntax
from typing import Optional, Union, TypeVar, Generic
example
from typing import TypeVar, Generic

T = TypeVar("T")

class Stack(Generic[T]):
    def __init__(self) -> None:
        self._items: list[T] = []

    def push(self, item: T) -> None:
        self._items.append(item)

    def pop(self) -> T:
        return self._items.pop()

stack = Stack[int]()
stack.push(42)
print(stack.pop())
output
42

Note Since 3.10, use X | Y instead of Union[X, Y]. Since 3.12, use the type parameter syntax: class Stack[T]: instead of TypeVar + Generic.

os and os.path

syntax
import os
os.path.join()
os.environ
example
import os

# Environment variables
db_host = os.environ.get("DB_HOST", "localhost")
print(f"DB: {db_host}")

# Path operations (prefer pathlib for new code)
print(os.path.expanduser("~"))
print(os.path.splitext("report.pdf"))
print(os.getcwd())
output
DB: localhost
/Users/username
('report', '.pdf')
/current/directory

Note Prefer pathlib.Path for path manipulation in new code. os.environ provides direct access to environment variables. Use os.getenv() for safe access with defaults.

json Module (Detailed)

syntax
json.dumps(obj, indent=2)
json.loads(string)
example
import json
from datetime import datetime

# Custom serializer for non-JSON types
def custom_encoder(obj):
    if isinstance(obj, datetime):
        return obj.isoformat()
    raise TypeError(f"Not serializable: {type(obj)}")

event = {"name": "Launch", "when": datetime(2026, 4, 4, 12, 0)}
print(json.dumps(event, default=custom_encoder, indent=2))
output
{
  "name": "Launch",
  "when": "2026-04-04T12:00:00"
}

Note json.dumps raises TypeError for non-serializable types. Pass default= for custom serialization. For complex cases, subclass json.JSONEncoder.

subprocess Module

syntax
import subprocess
subprocess.run([cmd, args], capture_output=True)
example
import subprocess

result = subprocess.run(
    ["echo", "Hello from subprocess"],
    capture_output=True, text=True
)
print(result.stdout.strip())
print(f"Return code: {result.returncode}")
output
Hello from subprocess
Return code: 0

Note Always pass commands as a list, not a string. Use shell=True only when absolutely necessary — it introduces shell injection risks.

Modern Features

Match/Case Pattern Types

syntax
match value:
    case pattern:
        ...
example
def describe(value):
    match value:
        case int(n) if n > 0:
            return f"positive int: {n}"
        case str(s) if len(s) > 5:
            return f"long string: {s!r}"
        case [first, *rest]:
            return f"list starting with {first}, {len(rest)} more"
        case {"type": "error", "code": code}:
            return f"error code {code}"
        case _:
            return "something else"

print(describe(42))
print(describe([1, 2, 3]))
print(describe({"type": "error", "code": 500}))
output
positive int: 42
list starting with 1, 2 more
error code 500

Note Match/case supports literal, capture, sequence, mapping, class, guard (if), and OR (|) patterns. The _ wildcard matches anything.

f-string Improvements (3.12+)

syntax
f"{'quoted'} {expr!r}"
example
# Nested quotes and backslashes now allowed (3.12+)
users = [{"name": "Alice"}, {"name": "Bob"}]
print(f"First user: {users[0]['name']}")

# Multi-line f-string expressions
result = f"Total: {
    sum(
        item['price']
        for item in [{'price': 10}, {'price': 20}]
    )
}"
print(result)
output
First user: Alice
Total: 30

Note Python 3.12 lifted the restriction on backslashes and same-quote nesting inside f-string expressions. This makes many previously awkward patterns simple.

Type Parameter Syntax (3.12+)

syntax
def func[T](arg: T) -> T: ...
class Name[T]: ...
example
# New syntax (3.12+) - no more TypeVar boilerplate
def first_element[T](items: list[T]) -> T:
    return items[0]

print(first_element([10, 20, 30]))
print(first_element(["a", "b"]))

# Generic class
class Pair[A, B]:
    def __init__(self, left: A, right: B):
        self.left = left
        self.right = right

p = Pair("name", 42)
print(p.left, p.right)
output
10
a
name 42

Note The [T] syntax on def/class replaces manual TypeVar declarations. It is scoped to the function or class, unlike module-level TypeVar which could be reused accidentally.

except* for Exception Groups

syntax
try:
    ...
except* ErrorType as eg:
    ...
example
import asyncio

async def task_a():
    raise ValueError("bad value")

async def task_b():
    raise TypeError("wrong type")

async def main():
    try:
        async with asyncio.TaskGroup() as tg:
            tg.create_task(task_a())
            tg.create_task(task_b())
    except* ValueError as eg:
        print(f"Value errors: {eg.exceptions}")
    except* TypeError as eg:
        print(f"Type errors: {eg.exceptions}")

# asyncio.run(main())
output
Value errors: (ValueError('bad value'),)
Type errors: (TypeError('wrong type'),)

Note except* can match multiple handlers for the same ExceptionGroup. It splits the group so each handler gets only its matching exceptions.

tomllib (Built-in TOML Parser, 3.11+)

syntax
import tomllib
with open('file.toml', 'rb') as f:
    data = tomllib.load(f)
example
import tomllib

toml_string = """
[project]
name = "myapp"
version = "1.0.0"
dependencies = ["requests", "click"]
"""

config = tomllib.loads(toml_string)
print(config["project"]["name"])
print(config["project"]["dependencies"])
output
myapp
['requests', 'click']

Note tomllib is read-only. To write TOML, use the third-party tomli-w package. Open TOML files in binary mode ('rb').

asyncio.TaskGroup (3.11+)

syntax
async with asyncio.TaskGroup() as tg:
    tg.create_task(coro())
example
import asyncio

async def fetch(url: str) -> str:
    await asyncio.sleep(0.1)
    return f"Data from {url}"

async def main():
    async with asyncio.TaskGroup() as tg:
        task1 = tg.create_task(fetch("/api/users"))
        task2 = tg.create_task(fetch("/api/orders"))

    print(task1.result())
    print(task2.result())

# asyncio.run(main())
output
Data from /api/users
Data from /api/orders

Note TaskGroup replaces asyncio.gather() with structured concurrency. If any task raises, all other tasks are cancelled and errors are collected into an ExceptionGroup.

Performance Features (3.13+)

syntax
# JIT compiler, free-threaded mode
example
# Python 3.13 introduced:
# 1. Experimental JIT compiler (--enable-experimental-jit)
# 2. Free-threaded mode (no GIL) via python3.13t
# 3. Improved error messages with color

import sys
print(f"Python {sys.version}")

# Check if GIL is disabled (3.13+)
# sys._is_gil_enabled()  # returns False in free-threaded build

Note Python 3.13's free-threaded build removes the GIL experimentally. Most C extensions need updating to work without the GIL. The JIT is opt-in and best for CPU-bound loops.

warnings.deprecated (3.13+)

syntax
from warnings import deprecated
@deprecated("Use new_func instead")
example
import warnings

# Python 3.13+
# from warnings import deprecated
# @deprecated("Use process_v2() instead")
# def process_v1(data): ...

# For earlier versions, manual approach:
def deprecate(msg):
    def decorator(func):
        def wrapper(*args, **kwargs):
            warnings.warn(f"{func.__name__}: {msg}", DeprecationWarning, stacklevel=2)
            return func(*args, **kwargs)
        return wrapper
    return decorator

@deprecate("Use process_v2() instead")
def process_v1(data):
    return data

process_v1("test")
output
# DeprecationWarning: process_v1: Use process_v2() instead

Note The built-in @deprecated decorator (3.13+, PEP 702) is recognized by type checkers, which will warn callers at analysis time, not just at runtime.

Common Mistakes

Mutable Default Arguments

syntax
# BAD
def f(items=[]):
# GOOD
def f(items=None):
example
# WRONG: Shared mutable default
def add_tag_bad(tag, tags=[]):
    tags.append(tag)
    return tags

print(add_tag_bad("a"))
print(add_tag_bad("b"))  # Surprise!

# CORRECT: Use None sentinel
def add_tag(tag, tags=None):
    if tags is None:
        tags = []
    tags.append(tag)
    return tags

print(add_tag("a"))
print(add_tag("b"))
output
['a']
['a', 'b']
['a']
['b']

Note Default values are evaluated once at function definition time, not on each call. Mutable defaults (list, dict, set) are shared across calls. Always use None as default.

Late Binding in Closures

syntax
# BAD: variable captured by reference
# GOOD: use default argument to capture value
example
# WRONG: All lambdas see final value of i
funcs_bad = [lambda: i for i in range(4)]
print([f() for f in funcs_bad])

# CORRECT: Capture current value via default arg
funcs_good = [lambda i=i: i for i in range(4)]
print([f() for f in funcs_good])
output
[3, 3, 3, 3]
[0, 1, 2, 3]

Note Python closures capture variables by reference, not by value. The loop variable's final value is what all closures see. The i=i default-argument trick forces a copy at each iteration.

Modifying a List During Iteration

syntax
# BAD: for x in items: items.remove(x)
# GOOD: iterate over a copy or build a new list
example
# WRONG: Modifying during iteration skips elements
nums = [1, 2, 3, 4, 5, 6]
for n in nums:
    if n % 2 == 0:
        nums.remove(n)
print(f"Wrong: {nums}")  # 4 was skipped!

# CORRECT: List comprehension to filter
nums = [1, 2, 3, 4, 5, 6]
result = [n for n in nums if n % 2 != 0]
print(f"Right: {result}")
output
Wrong: [1, 3, 5]
Right: [1, 3, 5]

Note Removing items while iterating shifts indices and causes skipped elements. Use a comprehension, filter(), or iterate over a copy (list[:]) instead.

is vs == (Identity vs Equality)

syntax
a == b  # equal values
a is b  # same object
example
a = [1, 2, 3]
b = [1, 2, 3]
print(a == b)
print(a is b)

# CPython caches small integers
x = 256
y = 256
print(x is y)

x = 257
y = 257
print(x is y)  # May be False!
output
True
False
True
False

Note Use == for value comparison. Use 'is' only for None, True, False, or when you specifically need identity checks. CPython caches integers -5 to 256, making 'is' unreliable for numbers.

Global vs Local Variable Confusion

syntax
global var_name  # to modify a global
nonlocal var_name  # to modify enclosing scope
example
counter = 0

def increment_wrong():
    # UnboundLocalError: counter referenced before assignment
    # counter += 1  # This fails!
    pass

def increment_correct():
    global counter
    counter += 1

def make_counter():
    count = 0
    def tick():
        nonlocal count
        count += 1
        return count
    return tick

tick = make_counter()
print(tick(), tick(), tick())
output
1 2 3

Note If a function assigns to a variable, Python treats it as local in the entire function. Reading a global is fine, but modifying requires 'global'. For enclosing scopes, use 'nonlocal'.

Shallow vs Deep Copy

syntax
import copy
shallow = copy.copy(obj)
deep = copy.deepcopy(obj)
example
import copy

original = [[1, 2], [3, 4]]
shallow = original.copy()
deep = copy.deepcopy(original)

original[0].append(99)
print(f"Original: {original}")
print(f"Shallow:  {shallow}")
print(f"Deep:     {deep}")
output
Original: [[1, 2, 99], [3, 4]]
Shallow:  [[1, 2, 99], [3, 4]]
Deep:     [[1, 2], [3, 4]]

Note list.copy(), dict.copy(), and slicing (lst[:]) all produce shallow copies. Nested mutable objects are still shared. Use copy.deepcopy() when you need fully independent nested structures.

Forgetting Strings Are Immutable

syntax
# BAD: s.replace(...) without reassigning
# GOOD: s = s.replace(...)
example
name = "  Alice  "

# WRONG: result is discarded
name.strip()
print(f"'{name}'")  # unchanged!

# CORRECT: reassign the result
name = name.strip()
print(f"'{name}'")
output
'  Alice  '
'Alice'

Note All string methods return new strings. Forgetting to capture the return value is a very common bug. The same applies to str.replace(), str.lower(), etc.

Truthiness Gotchas

syntax
# Falsy: None, False, 0, 0.0, '', [], {}, set()
example
def process(data=None):
    # WRONG: if not data
    # This also catches empty list [], 0, and ""
    if not data:
        print("No data")  # Triggered by empty list too!

    # CORRECT: if data is None
    if data is None:
        print("Data is None")

process([])   # Might not intend to reject []
process(None)
process(0)
output
No data
No data
Data is None
No data

Note Empty collections, zero, empty string, and None are all falsy. When you specifically mean 'no value provided', check 'is None' rather than relying on truthiness.