Variable Assignment
name = valueusername = "alice"
age = 30
is_active = True# alice, 30, TrueNote Python variables are references to objects, not fixed memory boxes. No declaration keyword is needed.
16 sections · 131 entries
name = valueusername = "alice"
age = 30
is_active = True# alice, 30, TrueNote Python variables are references to objects, not fixed memory boxes. No declaration keyword is needed.
name: type = valueprice: float = 19.99
items: list[str] = ["apple", "bread"]
user_map: dict[str, int] = {"alice": 1}# No runtime enforcement — hints are for tools and readersNote 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.
a, b, c = val1, val2, val3x, y, z = 10, 20, 30
first, *rest = [1, 2, 3, 4, 5]
print(first, rest)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(obj)
isinstance(obj, ClassName)score = 95.5
print(type(score))
print(isinstance(score, (int, float)))<class 'float'>
TrueNote Prefer isinstance() over type() == because isinstance handles subclasses correctly. The second arg can be a tuple of types.
variable = Noneresult = None
if result is None:
print("No result yet")
# Common pattern: optional return
def find_user(user_id: int) -> str | None:
return NoneNo result yetNote Always compare to None with 'is' or 'is not', never == or !=. None is a singleton object.
UPPER_SNAKE_CASE = valueMAX_RETRIES = 3
BASE_URL = "https://api.example.com"
PI = 3.14159Note 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.
name = value1
name = value2 # different type is alloweddata = 42
print(type(data))
data = "now a string"
print(type(data))<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 AliasName = existing_typetype 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.
f"text {expression}"name = "Alice"
balance = 1234.5
print(f"User: {name}")
print(f"Balance: ${balance:,.2f}")
print(f"{'centered':^20}")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.
str.method()email = " Alice@Example.COM "
print(email.strip().lower())
print("hello world".title())
print("python".startswith("py"))
print("2026-04-04".replace("-", "/"))alice@example.com
Hello World
True
2026/04/04Note String methods always return new strings since strings are immutable. Chain methods for compact transformations.
string[start:stop:step]word = "pythonic"
print(word[0:4])
print(word[-4:])
print(word[::2])
print(word[::-1])pyth
onic
ptoi
cinohtypNote Slicing never raises IndexError, even if indices exceed the string length. Negative step reverses direction.
"""text
spanning lines"""query = """
SELECT name, email
FROM users
WHERE active = true
""".strip()
print(query)SELECT name, email
FROM users
WHERE active = trueNote Triple-quoted strings preserve all whitespace and newlines. Use textwrap.dedent() or .strip() to control leading/trailing space.
r"text with \backslashes"path = r"C:\Users\alice\docs"
pattern = r"\d{3}-\d{4}"
print(path)
print(pattern)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.
str.encode(encoding)
bytes.decode(encoding)text = "caf\u00e9"
encoded = text.encode("utf-8")
print(encoded)
print(encoded.decode("utf-8"))b'caf\xc3\xa9'
caf\u00e9Note UTF-8 is the default encoding. Use errors='ignore' or errors='replace' to handle characters that cannot be encoded in the target encoding.
separator.join(iterable)
str.split(separator)words = ["Python", "is", "great"]
sentence = " ".join(words)
print(sentence)
csv_row = "alice,30,engineer"
fields = csv_row.split(",")
print(fields)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.
str.isdigit() / str.isalpha() / str.isalnum()pin = "4821"
print(pin.isdigit())
print("hello123".isalnum())
print(" ".isspace())
print("api" in "api_key_value")True
True
True
TrueNote The 'in' operator checks for substring presence and is usually more practical than the .is*() family for real-world validation.
x = 42 # int
y = 3.14 # floatcount = 1_000_000
ratio = 0.618
print(type(count), type(ratio))
print(count + ratio)<class 'int'> <class 'float'>
1000000.618Note Underscores in numeric literals are ignored and serve as visual separators. Python ints have unlimited precision.
+ - * / // % **print(17 / 5)
print(17 // 5)
print(17 % 5)
print(2 ** 10)3.4
3
2
1024Note / always returns a float. // is floor division (rounds toward negative infinity, not toward zero). So -7 // 2 gives -4, not -3.
abs(number)
round(number, ndigits)print(abs(-42.5))
print(round(3.14159, 2))
print(round(2.5))
print(round(3.5))42.5
3.14
2
4Note round() uses banker's rounding — it rounds to the nearest even number when the value is exactly halfway. This surprises many developers.
import mathimport math
print(math.ceil(4.2))
print(math.floor(4.8))
print(math.sqrt(144))
print(math.log(100, 10))
print(math.pi)5
4
12.0
2.0
3.141592653589793Note math functions work on ints and floats but not on complex numbers. Use cmath for complex math operations.
z = real + imagjz = 3 + 4j
print(z.real, z.imag)
print(abs(z))3.0 4.0
5.0Note abs() on a complex number returns its magnitude. Use the cmath module for complex-specific functions like phase and polar conversion.
int(x)
float(x)
complex(real, imag)print(int("42"))
print(int(9.99))
print(float("3.14"))
print(int("0xff", 16))42
9
3.14
255Note int() truncates toward zero (not floor). int('3.14') raises ValueError; convert to float first, then to int.
from decimal import Decimalfrom decimal import Decimal
# Floating point issue
print(0.1 + 0.2)
# Decimal precision
result = Decimal("0.1") + Decimal("0.2")
print(result)0.30000000000000004
0.3Note Always pass strings to Decimal(), not floats. Decimal(0.1) inherits the float imprecision. Critical for financial calculations.
f"{value:format_spec}"price = 1234567.891
print(f"{price:,.2f}")
print(f"{0.856:.1%}")
print(f"{42:08b}")
print(f"{255:#06x}")1,234,567.89
85.6%
00101010
0x00ffNote Format spec mini-language: comma for grouping, .Nf for decimal places, % for percent, b/o/x for binary/octal/hex.
items = [val1, val2, ...]
items = list(iterable)colors = ["red", "green", "blue"]
numbers = list(range(1, 6))
mixed = [1, "two", 3.0, True]
empty = []
print(numbers)[1, 2, 3, 4, 5]Note Lists can hold any mix of types. Use list() to convert other iterables (tuples, sets, generators) into lists.
items[index]
items[-index]fruits = ["apple", "banana", "cherry", "date"]
print(fruits[0])
print(fruits[-1])
print(fruits[-2])apple
date
cherryNote Index 0 is the first element, -1 is the last. Accessing an index beyond the list length raises IndexError.
items[start:stop:step]nums = [10, 20, 30, 40, 50, 60]
print(nums[1:4])
print(nums[::2])
print(nums[::-1])
nums[1:3] = [200, 300]
print(nums)[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.
list.append(item)
list.extend(iterable)
list.insert(index, item)tasks = ["email"]
tasks.append("report")
tasks.insert(0, "standup")
tasks.extend(["review", "deploy"])
print(tasks)['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]].
list.remove(value)
list.pop(index)
del list[index]items = ["a", "b", "c", "d", "e"]
items.remove("c")
print(items)
last = items.pop()
print(last, items)
del items[0]
print(items)['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.
list.sort(key=None, reverse=False)
sorted(iterable, key=None, reverse=False)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)[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.
[expression for item in iterable if condition]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)[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.
a, b, *rest = some_listcoords = [10, 20, 30, 40, 50]
x, y, *remaining = coords
print(x, y, remaining)
first, *_, last = coords
print(first, last)10 20 [30, 40, 50]
10 50Note Use _ as a throwaway variable name for values you do not need. The starred variable always produces a list, even if empty.
len() / in / enumerate() / zip()items = ["pen", "notebook", "eraser"]
print(len(items))
print("pen" in items)
for idx, item in enumerate(items, start=1):
print(f"{idx}. {item}")3
True
1. pen
2. notebook
3. eraserNote enumerate() gives (index, value) pairs. Pass start= to begin counting from a number other than 0.
d = {key: value, ...}
d = dict(key=value, ...)user = {"name": "Alice", "age": 30, "active": True}
from_pairs = dict(host="localhost", port=8080)
print(user)
print(from_pairs){'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.
d[key]
d[key] = valueconfig = {"debug": False, "port": 3000}
print(config["port"])
config["debug"] = True
config["host"] = "0.0.0.0"
print(config)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.
d.get(key, default)settings = {"theme": "dark"}
theme = settings.get("theme", "light")
lang = settings.get("language", "en")
print(theme, lang)dark enNote 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.
d.keys() / d.values() / d.items()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}")['alice', 'bob', 'carol']
[95, 82, 91]
alice: 95
bob: 82
carol: 91Note keys(), values(), and items() return view objects that reflect changes to the dict in real time.
{key_expr: val_expr for item in iterable if condition}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){'hello': 5, 'world': 5, 'python': 6}
{'b': 2, 'c': 3}Note Dict comprehensions are great for transforming or filtering dictionaries in a single expression.
merged = d1 | d2
d1 |= d2 # in-placedefaults = {"color": "blue", "size": "medium"}
overrides = {"size": "large", "bold": True}
final = defaults | overrides
print(final){'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.
from collections import defaultdict
dd = defaultdict(factory)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)){'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()).
from collections import Counterfrom collections import Counter
letters = Counter("mississippi")
print(letters.most_common(3))
inventory = Counter(apples=5, oranges=3)
inventory.update(apples=2)
print(inventory["apples"])[('s', 4), ('i', 4), ('p', 2)]
7Note Counter supports arithmetic: Counter('aab') - Counter('ab') gives Counter({'a': 1}). most_common() returns elements in descending frequency.
t = (val1, val2, ...)
t = val1, val2point = (10, 20)
singleton = (42,)
x, y = point
print(f"x={x}, y={y}")
print(len(singleton))x=10, y=20
1Note A single-element tuple requires a trailing comma: (42,). Without it, (42) is just an integer in parentheses. Tuples are immutable.
from collections import namedtuple
Point = namedtuple('Point', ['x', 'y'])from collections import namedtuple
Color = namedtuple("Color", ["red", "green", "blue"])
sky = Color(135, 206, 235)
print(sky.red, sky.blue)
print(sky._asdict())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.
from typing import NamedTuple
class Name(NamedTuple):
field: typefrom typing import NamedTuple
class Endpoint(NamedTuple):
host: str
port: int = 443
api = Endpoint("api.example.com")
print(api.host, api.port)api.example.com 443Note The class-based NamedTuple supports type annotations and default values, making it more modern than the functional namedtuple() form.
s = {val1, val2, ...}
s = set(iterable)tags = {"python", "tutorial", "beginner"}
from_list = set([1, 2, 2, 3, 3, 3])
print(from_list)
empty_set = set()
print(type(empty_set)){1, 2, 3}
<class 'set'>Note Use set() for an empty set, NOT {}. Empty braces {} create an empty dictionary, not a set.
a | b (union)
a & b (intersection)
a - b (difference)
a ^ b (symmetric difference)frontend = {"alice", "bob", "carol"}
backend = {"bob", "carol", "dave"}
print(frontend | backend)
print(frontend & backend)
print(frontend - backend)
print(frontend ^ backend){'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.
fs = frozenset(iterable)permissions = frozenset(["read", "write"])
print("read" in permissions)
# Can be used as a dictionary key
cache = {permissions: "admin"}
print(cache[frozenset(["read", "write"])])True
adminNote frozenset is hashable, so it can be used as a dict key or as an element inside another set. Regular sets cannot.
{expression for item in iterable if condition}emails = ["a@test.com", "B@TEST.COM", "a@test.com", "c@test.com"]
unique_lower = {e.lower() for e in emails}
print(unique_lower){'a@test.com', 'b@test.com', 'c@test.com'}Note Set comprehensions automatically deduplicate results. Useful for extracting unique transformed values from a collection.
s.add(elem)
s.discard(elem)
s.remove(elem)active = {"alice", "bob"}
active.add("carol")
active.discard("bob")
active.discard("nonexistent")
print(active){'alice', 'carol'}Note discard() silently ignores missing elements. remove() raises KeyError if the element is not found. Prefer discard() when absence is acceptable.
if condition:
...
elif condition:
...
else:
...score = 85
if score >= 90:
grade = "A"
elif score >= 80:
grade = "B"
elif score >= 70:
grade = "C"
else:
grade = "F"
print(grade)BNote Python uses indentation instead of braces. There is no switch statement in older Python; use match/case (3.10+) for pattern matching.
value_if_true if condition else value_if_falseage = 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)adult
youngNote Nested ternaries hurt readability fast. If you need more than one level, use a regular if/elif chain instead.
match subject:
case pattern:
...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")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 item in iterable:
...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}")Total: $45.49
Attempt 1
Attempt 2
Attempt 3Note 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 condition:
...attempts = 0
max_attempts = 3
while attempts < max_attempts:
attempts += 1
print(f"Try {attempts}")
print("Done")Try 1
Try 2
Try 3
DoneNote Make sure the condition eventually becomes False; otherwise you get an infinite loop. Use Ctrl+C to interrupt a runaway loop.
break # exit loop
continue # skip to next iterationfor 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}")Odd: 1
Odd: 3
Odd: 5
Found first multiple of 7: 7Note break exits only the innermost loop. To break out of nested loops, refactor into a function and use return.
for item in iterable:
...
else:
# runs if loop completed without breaktargets = [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")No multiple of 5 foundNote 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'.
if (name := expression):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)}")[12, 18, 22]
Order ID: 12345Note 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.
def name(params):
"""docstring"""
return valuedef greet(name: str, greeting: str = "Hello") -> str:
"""Build a personalized greeting."""
return f"{greeting}, {name}!"
print(greet("Alice"))
print(greet("Bob", greeting="Hey"))Hello, Alice!
Hey, Bob!Note Functions without an explicit return statement return None. Docstrings are accessible via help(func) and func.__doc__.
def func(*args, **kwargs):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")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.
def func(pos_only, /, normal, *, kw_only):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-onlyGET 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 params: expressionsquare = 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])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.
@decorator
def func(): ...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)compute took 0.0XXXsNote Always use @functools.wraps(func) in your wrapper so that the decorated function preserves its name and docstring. Decorators run at import time.
def outer():
captured = value
def inner():
return captured
return innerdef 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))20 30Note The inner function captures the enclosing variable by reference, not by value. See Common Mistakes for the late-binding closure gotcha.
def gen():
yield valuedef 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))3
2
1
1000000Note Generators produce values lazily, consuming almost no memory regardless of size. They are single-use: once exhausted, they cannot be restarted.
def func(param: Type) -> ReturnType: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)[10, 20, 30]Note Use collections.abc.Callable for function type hints. The format is Callable[[ArgTypes], ReturnType]. For complex signatures, consider typing.Protocol.
def func(n):
if base_case: return value
return func(modified_n)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()}")720
Recursion limit: 1000Note Python's default recursion limit is 1000. Deeply recursive algorithms should be rewritten iteratively or use sys.setrecursionlimit() with caution.
class Name:
def __init__(self, ...):
self.attr = valueclass 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}")Alice: $150Note self is not a keyword; it is a convention for the first parameter of instance methods. Python passes the instance automatically.
class Child(Parent):
def __init__(self):
super().__init__()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)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
def attr(self): ...
@attr.setter
def attr(self, value): ...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)212.0
32.0Note @property lets you expose computed values as attributes and add validation to setters without changing the caller's syntax.
@classmethod
def method(cls, ...): ...
@staticmethod
def method(...): ...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))2026 TrueNote @classmethod receives the class as first arg (cls) and is commonly used for alternative constructors. @staticmethod receives neither self nor cls.
from dataclasses import dataclass
@dataclass
class Name:
field: typefrom 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)Product(name='Widget', price=9.99, tags=['sale'])
$9.99Note Dataclasses auto-generate __init__, __repr__, and __eq__. Use field(default_factory=list) for mutable defaults, never a bare [] as a default.
class Name:
__slots__ = ('attr1', 'attr2')@dataclass(slots=True)
class Point:
x: float
y: float
p = Point(1.0, 2.0)
print(p)
# p.z = 3.0 # AttributeError: no __dict__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.
from abc import ABC, abstractmethod
class Base(ABC):
@abstractmethod
def method(self): ...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}")Area: 78.54Note Abstract classes cannot be instantiated directly. Subclasses must implement all @abstractmethod methods or they will also be abstract.
from typing import Protocol
class Drawable(Protocol):
def draw(self) -> None: ...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")Saved to /tmp/doc.txtNote Protocols enable duck-typing with static type checking. Classes do not need to explicitly inherit from the Protocol; they just need matching methods.
__str__, __repr__, __len__, __eq__, __lt__, __hash__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)))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.
with open(path, 'r') as f:
content = f.read()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.
with open(path, 'w') as f:
f.write(text)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).
from pathlib import Pathfrom 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){"debug": true}
.json settingsNote pathlib is the modern replacement for os.path. The / operator joins paths. Use .resolve() for absolute paths, .exists() to check existence.
import csvimport 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"])Alice 30
Bob 25Note Always pass newline='' when opening CSV files (the csv module handles line endings itself). DictReader/DictWriter are more readable than plain reader/writer.
import json
json.dump(obj, file)
obj = json.load(file)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"])8080Note 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.
with open(path, 'rb') as f:
data = f.read()# 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.
with expression as variable:
...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.
Path(dir).glob(pattern)
Path(dir).rglob(pattern)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.
try:
...
except ExceptionType as e:
...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}")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.
try:
...
except:
...
else:
...
finally:
...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)Result: 3.3333333333333335
Division attempted
Cannot divide by zero
Division attemptedNote else runs only when no exception occurred. finally always runs, even after return statements. Use else to keep the try block minimal.
raise ExceptionType(message)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)Insufficient fundsNote Use raise without arguments inside an except block to re-raise the current exception. Use 'raise NewError() from original' to chain exceptions.
class MyError(Exception):
...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}")Payment of $49.99 failed: card declined
Amount: $49.99Note Custom exceptions should inherit from Exception (not BaseException). Add meaningful attributes so callers can inspect the error programmatically.
raise ExceptionGroup(msg, [exc1, exc2])
except* ErrorType as eg: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}") - name is required
- email is requiredNote except* catches matching exceptions from the group while letting others propagate. ExceptionGroup is ideal for concurrent/batch operations that produce multiple errors.
raise NewError() from original_errorclass DatabaseError(Exception):
pass
try:
int("bad")
except ValueError as e:
raise DatabaseError("Failed to parse DB record") from e# Traceback shows both: ValueError → DatabaseErrorNote Using 'from e' sets __cause__ so the traceback shows the chain. Use 'from None' to explicitly suppress the original exception context.
ValueError, TypeError, KeyError, IndexError, ...# 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")File not foundNote Learn the hierarchy: FileNotFoundError is a subclass of OSError. Catching OSError also catches FileNotFoundError, PermissionError, etc.
from contextlib import suppress
with suppress(ExceptionType):
...from contextlib import suppress
import os
# Instead of try/except/pass
with suppress(FileNotFoundError):
os.remove("temp_cache.dat")
print("Cleanup done")Cleanup doneNote contextlib.suppress is cleaner than try/except/pass when you intentionally want to ignore specific exceptions. Do not suppress broad exception types.
import module
from module import name
import module as aliasimport json
from pathlib import Path
from collections import defaultdict, Counter
import numpy as np # common alias convention
data = json.dumps({"key": "value"})
print(data){"key": "value"}Note Convention: standard library imports first, then third-party, then local. Separate groups with a blank line. Use isort to auto-format.
if __name__ == '__main__':
...# 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}")Tax: $8.00Note This guard prevents code from running when the module is imported by another file. Essential for reusable modules that also serve as scripts.
__all__ = ['name1', 'name2']# 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
passNote __all__ defines what 'from module import *' exports. It does not prevent direct imports of unlisted names. Prefix internal helpers with underscore by convention.
from . import sibling
from .. import parent_module
from .sibling import name# project/
# ├── __init__.py
# ├── models.py
# └── services/
# ├── __init__.py
# └── auth.py
# Inside services/auth.py:
# from ..models import User
# from . import helpersNote 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.
python -m venv env_name
source env_name/bin/activate# 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
# deactivateNote Always use virtual environments to isolate project dependencies. Python 3.12+ also has improved error messages when venv is not activated.
package/
__init__.py
module.py
subpackage/
__init__.py# 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 UserNote __init__.py runs when the package is imported. Use it to define the package's public interface. It can be empty for simple packages.
import importlib
mod = importlib.import_module('name')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){"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 install package
pip install -r requirements.txt# 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 requestsNote Consider using pip-tools, poetry, or uv for reproducible dependency management. pip freeze captures transitive deps, which can be fragile across platforms.
[expr for x in iterable if cond]# 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)[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.
{key_expr: val_expr for item in iterable if cond}# 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)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.
{expr for item in iterable if cond}sentence = "the quick brown fox jumps over the lazy dog"
word_lengths = {len(word) for word in sentence.split()}
print(sorted(word_lengths))[3, 4, 5]Note Set comprehensions look like dict comprehensions but without the colon. They automatically deduplicate results.
(expr for item in iterable if cond)# 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")338350
List: ~85000 bytes, Generator: ~200 bytesNote 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 iterabledef 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)))[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.
import itertoolsfrom 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)[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.
from itertools import groupby
for key, group in groupby(iterable, key=func):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}")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.
[y := f(x) ... if y > threshold]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)[(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.
from datetime import datetime, date, timedeltafrom 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())2026-04-04 14:30
Due: April 11, 2026
2026-04-04Note For timezone-aware datetimes, use datetime.now(tz=timezone.utc) instead of datetime.utcnow() which is naive and deprecated since 3.12.
from collections import deque, OrderedDict, ChainMapfrom 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"])[2, 3, 4]
red 10Note deque with maxlen auto-discards oldest items when full (ring buffer). ChainMap searches dicts in order, first match wins. Both are in collections.
from functools import lru_cache, partial, reducefrom 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))832040
CacheInfo(hits=28, misses=31, maxsize=128, currsize=31)
49Note lru_cache requires hashable arguments. For unhashable args, consider cachetools or a manual cache. partial is great for adapting callback signatures.
import re
re.search(pattern, string)
re.findall(pattern, string)import re
text = "Contact: alice@mail.com or bob@work.org"
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"))['alice@mail.com', 'bob@work.org']
ERROR Connection timeoutNote 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.
from typing import Optional, Union, TypeVar, Genericfrom 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())42Note 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.
import os
os.path.join()
os.environimport 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())DB: localhost
/Users/username
('report', '.pdf')
/current/directoryNote 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.dumps(obj, indent=2)
json.loads(string)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)){
"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.
import subprocess
subprocess.run([cmd, args], capture_output=True)import subprocess
result = subprocess.run(
["echo", "Hello from subprocess"],
capture_output=True, text=True
)
print(result.stdout.strip())
print(f"Return code: {result.returncode}")Hello from subprocess
Return code: 0Note Always pass commands as a list, not a string. Use shell=True only when absolutely necessary — it introduces shell injection risks.
match value:
case pattern:
...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}))positive int: 42
list starting with 1, 2 more
error code 500Note Match/case supports literal, capture, sequence, mapping, class, guard (if), and OR (|) patterns. The _ wildcard matches anything.
f"{'quoted'} {expr!r}"# 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)First user: Alice
Total: 30Note Python 3.12 lifted the restriction on backslashes and same-quote nesting inside f-string expressions. This makes many previously awkward patterns simple.
def func[T](arg: T) -> T: ...
class Name[T]: ...# 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)10
a
name 42Note 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.
try:
...
except* ErrorType as eg:
...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())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.
import tomllib
with open('file.toml', 'rb') as f:
data = tomllib.load(f)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"])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').
async with asyncio.TaskGroup() as tg:
tg.create_task(coro())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())Data from /api/users
Data from /api/ordersNote TaskGroup replaces asyncio.gather() with structured concurrency. If any task raises, all other tasks are cancelled and errors are collected into an ExceptionGroup.
# JIT compiler, free-threaded mode# 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 buildNote 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.
from warnings import deprecated
@deprecated("Use new_func instead")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")# DeprecationWarning: process_v1: Use process_v2() insteadNote 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.
# BAD
def f(items=[]):
# GOOD
def f(items=None):# 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"))['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.
# BAD: variable captured by reference
# GOOD: use default argument to capture value# 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])[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.
# BAD: for x in items: items.remove(x)
# GOOD: iterate over a copy or build a new list# 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}")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.
a == b # equal values
a is b # same objecta = [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!True
False
True
FalseNote 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 var_name # to modify a global
nonlocal var_name # to modify enclosing scopecounter = 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())1 2 3Note 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'.
import copy
shallow = copy.copy(obj)
deep = copy.deepcopy(obj)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}")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.
# BAD: s.replace(...) without reassigning
# GOOD: s = s.replace(...)name = " Alice "
# WRONG: result is discarded
name.strip()
print(f"'{name}'") # unchanged!
# CORRECT: reassign the result
name = name.strip()
print(f"'{name}'")' 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.
# Falsy: None, False, 0, 0.0, '', [], {}, set()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)No data
No data
Data is None
No dataNote 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.