Data Types and Variables
# Primitives
x: int = 42
pi: float = 3.14
flag: bool = True # True / False (capitalised)
name: str = "ada"
nothing: None = None
# Type aliases (3.12+)
type Vector = list[float] # new `type` statement, not TypeAlias
# Multiple assignment
a, b, c = 1, 2, 3
a, *rest = [1, 2, 3, 4] # rest = [2, 3, 4]
_, important, *_ = data # discard with _
# Walrus operator (3.8+)
if n := len(data):
print(f"got {n} items")
Strings and f-strings
s = "hello world"
# Common methods
s.upper() # "HELLO WORLD"
s.split() # ["hello", "world"]
s.replace("world", "python")
s.strip() # trim whitespace
s.startswith("hel") # True
s.find("world") # 6 (-1 if not found)
",".join(["a","b"]) # "a,b"
# f-string formatting (3.12 allows nested quotes inside {})
name, score = "Ada", 98.5
f"{name!r} scored {score:.1f}%" # "'Ada' scored 98.5%"
f"{score:>10.2f}" # right-align, 2 decimals
f"{2**10 = }" # "2**10 = 1024" (debug =)
f"{'yes' if score > 90 else 'no'}" # inline expression
# Multi-line / raw
path = r"C:\Users\ada" # raw — backslashes literal
blob = """
line one
line two
"""
| Method | Returns | Notes |
|---|
s.split(sep, maxsplit) | list | default sep = any whitespace |
s.partition(sep) | (before, sep, after) | always 3-tuple |
s.encode("utf-8") | bytes | inverse: b.decode() |
s.zfill(width) | str | zero-pad numbers |
Lists
nums = [3, 1, 4, 1, 5, 9]
nums.append(2) # add to end
nums.extend([6, 5]) # merge another iterable
nums.insert(0, 0) # insert at index
nums.pop() # remove+return last
nums.pop(2) # remove+return at index
nums.remove(1) # remove first occurrence of value
nums.index(5) # position of value
nums.count(1) # frequency
nums.sort(reverse=True) # in-place
sorted(nums) # returns new list (original unchanged)
nums.reverse() # in-place
nums.copy() # shallow copy
Tuples and Sets
# Tuple — immutable sequence
point = (1, 2)
single = (42,) # trailing comma required for length-1
x, y = point # unpack
# Named tuple
from typing import NamedTuple
class Point(NamedTuple):
x: float
y: float
p = Point(1.0, 2.0)
p.x # 1.0
# Set — unique, unordered
seen = {1, 2, 3}
seen.add(4)
seen.discard(99) # no error if missing (remove() raises)
a | b # union
a & b # intersection
a - b # difference
a ^ b # symmetric difference
frozenset({1, 2}) # immutable set, hashable
Dictionaries
d = {"a": 1, "b": 2}
# Access
d["a"] # 1 (KeyError if missing)
d.get("z", 0) # 0 (default, no error)
d.setdefault("c", []) # insert+return default if key absent
# Mutation
d["c"] = 3
del d["a"]
d.update({"d": 4, "e": 5})
d.pop("b", None) # remove+return, None if missing
# Views (live, not copies)
d.keys()
d.values()
d.items() # use in for-loops: for k, v in d.items()
# Merge (3.9+)
merged = d | {"f": 6} # new dict
d |= {"g": 7} # in-place
# dict comprehension
squares = {n: n**2 for n in range(6)}
Slicing
# seq[start:stop:step] — stop is exclusive
s = [0, 1, 2, 3, 4, 5]
s[1:4] # [1, 2, 3]
s[:3] # [0, 1, 2]
s[3:] # [3, 4, 5]
s[-2:] # [4, 5] last two
s[::2] # [0, 2, 4] every other
s[::-1] # [5, 4, 3, 2, 1, 0] reversed copy
# Works identically on str, bytes, tuple
"hello"[1:4] # "ell"
# Named slice for reuse
weekly = slice(0, 7)
data[weekly]
Comprehensions
# List comprehension
evens = [x for x in range(20) if x % 2 == 0]
# Nested
flat = [n for row in matrix for n in row]
# Dict comprehension
inv = {v: k for k, v in mapping.items()}
# Set comprehension
unique_lens = {len(w) for w in words}
# Generator expression — lazy, no brackets
total = sum(x**2 for x in range(1_000_000)) # no list built
# Walrus in comprehension (avoids double-call)
results = [y for x in data if (y := expensive(x)) is not None]
Control Flow
# if / elif / else
if x > 0:
...
elif x == 0:
...
else:
...
# Ternary
label = "positive" if x > 0 else "non-positive"
# match / case (3.10+)
match command.split():
case ["quit"]:
quit()
case ["go", direction]:
move(direction)
case ["pick", item, *rest]:
pick_up(item, rest)
case _:
print("unknown")
# for / else — else runs if loop not broken
for item in iterable:
if condition(item):
break
else:
print("nothing matched")
# while
while queue:
process(queue.pop(0))
Functions
# Basic signature with type hints
def greet(name: str, *, loud: bool = False) -> str:
msg = f"Hello, {name}"
return msg.upper() if loud else msg
# Positional-only (before /) and keyword-only (after *)
def f(pos_only, /, normal, *, kw_only):
...
# *args and **kwargs
def log(*args, level="INFO", **kwargs):
print(level, args, kwargs)
log("started", "app", version=2)
# Unpacking at call site
nums = [1, 2, 3]
print(*nums) # same as print(1, 2, 3)
func(**{"a": 1, "b": 2})
# Lambda — single expression only
double = lambda x: x * 2
sorted(pairs, key=lambda p: p[1])
# Default mutable argument trap — use None
def add_item(item, lst=None): # NOT lst=[]
if lst is None:
lst = []
lst.append(item)
return lst
Built-in Functions
# enumerate — index + value
for i, val in enumerate(items, start=1):
print(i, val)
# zip — parallel iteration (stops at shortest)
for a, b in zip(list1, list2):
...
dict(zip(keys, values)) # quick dict from two lists
# zip_longest fills missing with fillvalue
from itertools import zip_longest
for a, b in zip_longest(l1, l2, fillvalue=0):
...
# sorted + key
sorted(words, key=str.lower)
sorted(records, key=lambda r: (r.year, r.name))
# map / filter (return iterators)
squares = list(map(lambda x: x**2, nums))
odds = list(filter(lambda x: x % 2, nums))
# prefer comprehensions — more readable:
squares = [x**2 for x in nums]
# any / all
any(x > 0 for x in nums) # True if at least one
all(x > 0 for x in nums) # True if all
# min / max with key
max(words, key=len)
# sum, abs, round, pow, divmod
divmod(17, 5) # (3, 2) quotient and remainder
# vars, dir, type, isinstance
isinstance(x, (int, float)) # check multiple types
Error Handling
# Full structure
try:
result = risky_call()
except FileNotFoundError as e:
print(f"file missing: {e}")
except (ValueError, TypeError) as e:
raise RuntimeError("bad input") from e # chain
except Exception:
raise # re-raise unchanged
else:
process(result) # runs only if no exception
finally:
cleanup() # always runs
# Raise
raise ValueError(f"expected positive, got {x}")
# Custom exception
class AppError(Exception):
def __init__(self, msg, code=500):
super().__init__(msg)
self.code = code
# Exception groups (3.11+)
try:
async with asyncio.TaskGroup() as tg:
...
except* ValueError as eg:
for exc in eg.exceptions:
handle(exc)
Classes and Dataclasses
class Animal:
species: str = "unknown" # class variable
def __init__(self, name: str, age: int):
self.name = name
self.age = age
def __repr__(self) -> str:
return f"Animal({self.name!r}, {self.age})"
def __eq__(self, other) -> bool:
return isinstance(other, Animal) and self.name == other.name
@classmethod
def from_dict(cls, data: dict) -> "Animal":
return cls(data["name"], data["age"])
@staticmethod
def is_valid_age(age: int) -> bool:
return age >= 0
@property
def summary(self) -> str:
return f"{self.name} ({self.age}y)"
# Dataclass — auto __init__, __repr__, __eq__
from dataclasses import dataclass, field
@dataclass(order=True, slots=True) # slots=True: 3.10+
class Point:
x: float
y: float
tags: list[str] = field(default_factory=list)
def distance(self) -> float:
return (self.x**2 + self.y**2) ** 0.5
# frozen=True makes it immutable (and hashable)
@dataclass(frozen=True)
class Color:
r: int
g: int
b: int
Decorators
import functools
# Simple decorator
def log_calls(func):
@functools.wraps(func) # preserve __name__, __doc__
def wrapper(*args, **kwargs):
print(f"calling {func.__name__}")
return func(*args, **kwargs)
return wrapper
@log_calls
def add(a, b):
return a + b
# Decorator with arguments
def retry(times=3):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(times):
try:
return func(*args, **kwargs)
except Exception:
if attempt == times - 1:
raise
return wrapper
return decorator
@retry(times=5)
def fetch(url): ...
# Class-based decorator
class Cache:
def __init__(self, func):
functools.update_wrapper(self, func)
self.func = func
self._cache = {}
def __call__(self, *args):
if args not in self._cache:
self._cache[args] = self.func(*args)
return self._cache[args]
# stdlib decorators
from functools import lru_cache, cached_property
@lru_cache(maxsize=128)
def fib(n): return n if n < 2 else fib(n-1) + fib(n-2)
class Circle:
def __init__(self, r): self.r = r
@cached_property # computed once, then stored on instance
def area(self): return 3.14159 * self.r ** 2
Context Managers
# with guarantees __exit__ runs even on exception
with open("data.txt") as f:
content = f.read()
# Multiple resources in one line (3.10+ parenthesised form)
with (
open("input.txt") as src,
open("output.txt", "w") as dst,
):
dst.write(src.read())
# contextlib.contextmanager — generator-based
from contextlib import contextmanager, suppress
@contextmanager
def timer(label=""):
import time
start = time.perf_counter()
try:
yield
finally:
print(f"{label} {time.perf_counter() - start:.3f}s")
with timer("sort"):
data.sort()
# suppress — swallow specific exceptions
with suppress(FileNotFoundError):
os.remove("tmp.txt")
File I/O and pathlib
from pathlib import Path
p = Path("/tmp/data")
p.mkdir(parents=True, exist_ok=True)
f = p / "notes.txt" # / operator builds paths
f.write_text("hello\n") # creates or overwrites
f.read_text() # "hello\n"
f.read_bytes()
f.exists() # bool
f.is_file() / f.is_dir()
f.stat().st_size # bytes
f.suffix # ".txt"
f.stem # "notes"
f.parent # Path("/tmp/data")
f.rename(p / "notes2.txt")
f.unlink(missing_ok=True)
# Glob
for csv in Path(".").rglob("*.csv"):
print(csv)
# Open with encoding
with open(f, encoding="utf-8") as fh:
for line in fh: # iterate lines without loading all
process(line.rstrip())
# Write lines
lines = ["a\n", "b\n", "c\n"]
f.write_text("".join(lines))
Useful stdlib
# collections.Counter
from collections import Counter, defaultdict, deque
words = "to be or not to be".split()
c = Counter(words)
c.most_common(2) # [("to", 2), ("be", 2)]
c["to"] # 2 (missing keys return 0)
c.update(["to", "also"])
# defaultdict — auto-creates missing keys
graph = defaultdict(list)
graph["a"].append("b") # no KeyError
# deque — O(1) append/pop from both ends
dq = deque([1, 2, 3], maxlen=5)
dq.appendleft(0)
dq.rotate(1)
# itertools
from itertools import chain, islice, groupby, product, combinations, permutations
list(chain([1,2], [3,4])) # [1, 2, 3, 4]
list(islice(range(1000), 5)) # [0,1,2,3,4] lazy head
list(combinations("ABC", 2)) # [("A","B"),("A","C"),("B","C")]
list(product([0,1], repeat=3)) # all 3-bit binary combos
for key, group in groupby(sorted(data), key=lambda x: x[0]):
print(key, list(group))
# datetime
from datetime import datetime, date, timedelta, timezone
now = datetime.now(tz=timezone.utc)
today = date.today()
delta = timedelta(days=7)
next_week = today + delta
now.strftime("%Y-%m-%d %H:%M")
datetime.strptime("2026-06-06", "%Y-%m-%d")
# json
import json
text = json.dumps({"key": [1, 2]}, indent=2)
data = json.loads(text)
Path("out.json").write_text(json.dumps(data))
# os / sys
import os, sys
os.environ.get("HOME", "/tmp")
os.getcwd()
sys.argv # command-line args including script name
sys.exit(1) # exit with code
List vs Generator Memory
# List — builds entire sequence in RAM immediately
big_list = [x**2 for x in range(10_000_000)] # ~400 MB
# Generator — yields one item at a time, O(1) memory
big_gen = (x**2 for x in range(10_000_000)) # ~200 bytes
# Consume lazily
total = sum(big_gen) # never materialises full sequence
# Generator function
def squares(n):
for i in range(n):
yield i ** 2
# Use itertools.islice to peek
from itertools import islice
first5 = list(islice(squares(1_000_000), 5))
# When to materialise
items = list(gen) # if you need to iterate multiple times
# or need len(), indexing, or random access
Virtual Environments
# uv — fast Rust-based tool (recommended)
# uv add / uv run replace pip in uv-managed projects
# Install uv
# curl -LsSf https://astral.sh/uv/install.sh | sh
# New project
# uv init myproject && cd myproject
# uv add requests pandas # adds to pyproject.toml
# uv run python main.py # runs inside managed venv
# Standalone venv (no uv)
# python -m venv .venv
# source .venv/bin/activate # macOS/Linux
# .venv\Scripts\activate # Windows
# pip install -r requirements.txt
# pip essentials
# pip install package==1.2.3
# pip install -e . # editable install for local package
# pip list --outdated
# pip freeze > requirements.txt
# pyproject.toml dependency spec (PEP 621)
# [project]
# requires-python = ">=3.12"
# dependencies = ["requests>=2.31", "pandas~=2.2"]
Type Hints (3.12+)
from typing import TypeVar, Protocol, TYPE_CHECKING
# Built-in generics — no import needed (3.9+)
def process(items: list[int]) -> dict[str, int]: ...
# Union via | (3.10+)
def handle(val: int | str | None) -> str: ...
# TypeVar
T = TypeVar("T")
def first(seq: list[T]) -> T:
return seq[0]
# ParamSpec and TypeVarTuple (3.12 type statement)
type Matrix[T] = list[list[T]] # generic alias
# Protocol — structural subtyping (duck-typing friendly)
class Drawable(Protocol):
def draw(self) -> None: ...
def render(obj: Drawable) -> None:
obj.draw()
# TYPE_CHECKING guard — avoids circular imports
if TYPE_CHECKING:
from mymodule import HeavyClass
def foo(x: "HeavyClass") -> None: ... # forward reference as string
Common Patterns and Idioms
# Swap without temp
a, b = b, a
# Safe dict traverse
value = data.get("user", {}).get("email", "unknown")
# Flatten one level
flat = sum(nested, []) # or: list(chain.from_iterable(nested))
# Deduplicate while preserving order
seen = set()
unique = [x for x in items if not (x in seen or seen.add(x))]
# Chunk a list
def chunks(lst, n):
for i in range(0, len(lst), n):
yield lst[i:i+n]
# Merge dicts, later keys win
result = {**defaults, **overrides} # or: defaults | overrides (3.9+)
# Conditional dict insertion
extra = {"debug": True} if verbose else {}
config = {**base, **extra}
# Count by category
from collections import Counter
by_type = Counter(type(x).__name__ for x in objects)
# Default via or (careful: 0 and "" are falsy)
name = user_input or "anonymous" # OK for strings
# Sentinel for truly-absent default
_MISSING = object()
def get(d, key, default=_MISSING):
val = d.get(key, _MISSING)
if val is _MISSING:
if default is _MISSING:
raise KeyError(key)
return default
return val