datarekha

Python cheat sheet

Core Python syntax, data structures, comprehensions, functions, and idioms — the essentials on one page.

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
"""
MethodReturnsNotes
s.split(sep, maxsplit)listdefault sep = any whitespace
s.partition(sep)(before, sep, after)always 3-tuple
s.encode("utf-8")bytesinverse: b.decode()
s.zfill(width)strzero-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
Go deeper The full Python course →

Explore further

Skip to content