The Complete Python Cheat Sheet — Beginner to Pro

A comprehensive, interactive Python reference covering 26 parts — from Hello World to metaclasses — built by a developer with 20+ years of Python teaching experience. Every section is collapsible and searchable. Every code snippet is runnable in your browser via Pyodide. Includes 30+ "Common Mistake vs. Pythonic Way" comparisons, a live code editor, dark mode, and a streamer mode designed for live coding on YouTube. Covers Python 3.10–3.13 features including structural pattern matching, the walrus operator, and the new union type syntax.

Press / or Cmd+K to search · s Stream Mode · d Dark/Light · ▶ Run to execute code in-browser

0/26 done

Install & Run

Python 3.10+ is recommended. Download from python.org or use a version manager.

Shell
# Check version
python --version   # or python3 --version

# Interactive REPL
python

# Run a script
python hello.py

# Run a module
python -m http.server 8080
💡 Use pyenv (Linux/Mac) or pyenv-win (Windows) to manage multiple Python versions side-by-side.
Package Management
# pip — Python package manager
pip install requests           # install
pip install requests==2.31.0  # specific version
pip install -r requirements.txt
pip list                       # installed packages
pip show requests              # package info
pip uninstall requests

Hello World & Conventions

hello.py
print("Hello, World!")
print("Python", 3.12, "rocks")
name = "Alice"
print(f"Hello, {name}!")
ConventionExampleUsed for
snake_caseuser_name, get_data()Variables, functions, modules
PascalCaseUserProfile, HttpClientClasses
SCREAMING_SNAKEMAX_RETRIES, API_URLConstants
_single_leading_internal_method()Private by convention
__double_leading__slots__Name-mangled class attrs
💡 PEP 8 is the official style guide. Run `pip install ruff` and use `ruff check .` for instant linting.

Virtual Environments

Shell
# Create
python -m venv .venv

# Activate
source .venv/bin/activate      # Linux/Mac
.venv\Scripts\activate       # Windows PowerShell

# Deactivate
deactivate

# Modern alternative: uv (10x faster than pip)
pip install uv
uv venv
uv pip install requests
Common Mistake
✗ Avoid
# Installing packages globally
pip install flask
✓ Pythonic
# Always use a virtual environment
python -m venv .venv && source .venv/bin/activate
pip install flask

Global installs pollute your system Python and cause version conflicts across projects.

Core Types

# Numbers
x = 42          # int
y = 3.14        # float
z = 2 + 3j      # complex

# Strings
s = "hello"
raw = r"C:\Users\no\escaping"

# Booleans (capitalized!)
t = True
f = False

# None — the null value
n = None

# Check type
print(type(x))   # <class 'int'>
print(isinstance(x, int))  # True
TypeLiteral exampleMutable?
int42, -7, 0No
float3.14, 1e10, float("inf")No
complex2+3jNo
str"hello", 'world'No
boolTrue, FalseNo
NoneTypeNoneNo
list[1, 2, 3]Yes
tuple(1, 2, 3)No
dict{"a": 1}Yes
set{1, 2, 3}Yes
frozensetfrozenset({1, 2})No
bytesb"hello"No
bytearraybytearray(b"hello")Yes

Numbers & Math

print(10 // 3)    # 3  — floor division
print(10 % 3)     # 1  — modulo
print(2 ** 10)    # 1024 — exponentiation
print(abs(-5))    # 5
print(round(3.14159, 2))  # 3.14

# Python ints are arbitrary precision
print(2 ** 100)

# Underscores for readability
million = 1_000_000
pi_approx = 3.141_592_6

import math
print(math.floor(4.9))   # 4
print(math.ceil(4.1))    # 5
print(math.sqrt(16))     # 4.0
print(math.log(math.e))  # 1.0
Common Mistake
✗ Avoid
0.1 + 0.2 == 0.3   # False — floating point!
✓ Pythonic
import math
math.isclose(0.1 + 0.2, 0.3)   # True

# Or use decimal for exact arithmetic
from decimal import Decimal
Decimal("0.1") + Decimal("0.2")  # Decimal('0.3')

Float comparison is never exact due to IEEE 754 binary representation.

Assignment & Scope

# Multiple assignment
a, b, c = 1, 2, 3
x = y = z = 0

# Swap without temp variable
a, b = b, a
print(a, b)

# Augmented assignment
n = 10
n += 5   # n = 15
n *= 2   # n = 30
n //= 4  # n = 7

# Walrus operator := (Python 3.8+)
data = [1, 2, 3, 4, 5]
if (n := len(data)) > 3:
    print(f"List is long: {n} items")
💡 Python has no constants — SCREAMING_SNAKE_CASE is a social contract, not enforcement. Use `Final` from `typing` for IDE/type-checker enforcement.

String Literals & Formatting

name = "Alice"
age  = 30
pi   = 3.14159

# f-string (preferred, Python 3.6+)
print(f"Hello, {name}! You are {age}.")
print(f"{pi:.2f}")          # 3.14
print(f"{age:>10}")         # right-align in 10 chars
print(f"{1_000_000:,}")     # 1,000,000
print(f"{0.007:.2%}")       # 0.70%

# f-string debug (Python 3.8+)
x = 42
print(f"{x=}")   # x=42

# Multiline
poem = """
Roses are red,
Violets are blue.
"""

# Raw strings (no escape processing)
path = r"C:\Users\Alice\file.txt"
Common Mistake
✗ Avoid
msg = "Hello, " + name + "! You are " + str(age) + "."
✓ Pythonic
msg = f"Hello, {name}! You are {age}."

String concatenation with + is slow and hard to read. f-strings are faster and clearer.

Common String Methods

s = "  Hello, World!  "

# Case
print(s.strip())            # "Hello, World!"
print(s.lower())            # "  hello, world!  "
print(s.upper())
print("hello world".title()) # "Hello World"

# Search
print("World" in s)         # True
print(s.find("World"))      # 9
print(s.count("l"))         # 3
print(s.startswith("  H"))  # True

# Modify (returns new string — strings are immutable)
print(s.replace("World", "Python"))
print(",".join(["a", "b", "c"]))  # "a,b,c"
print("a,b,c".split(","))         # ['a', 'b', 'c']
print("a,b,,c".split(","))        # ['a', 'b', '', 'c']

# Padding
print("42".zfill(5))    # "00042"
print("hi".center(10, "-"))  # "----hi----"
💡 `str.split()` with no args splits on any whitespace and removes empty strings — useful for parsing user input.

Slicing & Indexing

s = "Python"
#    P  y  t  h  o  n
#    0  1  2  3  4  5
#   -6 -5 -4 -3 -2 -1

print(s[0])      # 'P'
print(s[-1])     # 'n'
print(s[1:4])    # 'yth'
print(s[:3])     # 'Pyt'
print(s[3:])     # 'hon'
print(s[::2])    # 'Pto'  (every 2nd char)
print(s[::-1])   # 'nohtyP'  (reverse)

# Strings are immutable — you can't assign to s[0]
# Convert to list, mutate, join back
chars = list("Python")
chars[0] = "J"
print("".join(chars))  # "Jython"

Lists

fruits = ["apple", "banana", "cherry"]

# Access
print(fruits[0])     # "apple"
print(fruits[-1])    # "cherry"

# Mutate
fruits.append("date")
fruits.insert(1, "avocado")
fruits.remove("banana")   # removes first match
popped = fruits.pop()     # removes & returns last
popped2 = fruits.pop(0)   # removes & returns at index

# Sort
nums = [3, 1, 4, 1, 5, 9]
nums.sort()                        # in-place
sorted_nums = sorted(nums)         # new list
nums.sort(reverse=True)
words = ["banana", "apple", "cherry"]
words.sort(key=len)                # sort by length

# Other
print(len(fruits))
print(3 in nums)
fruits.reverse()
print(fruits.count("apple"))
Common Mistake
✗ Avoid
# Checking if list is empty
if len(my_list) == 0:
    print("empty")
✓ Pythonic
if not my_list:
    print("empty")

Empty collections are falsy in Python. `not my_list` is idiomatic.

Tuples

# Tuples are immutable lists — great for fixed data
point = (3, 4)
x, y = point          # unpacking

# Named tuples — tuple with field names
from collections import namedtuple
Color = namedtuple("Color", ["r", "g", "b"])
red = Color(255, 0, 0)
print(red.r, red.g, red.b)

# Modern: dataclasses or typing.NamedTuple
from typing import NamedTuple

class Point(NamedTuple):
    x: float
    y: float
    label: str = ""

p = Point(1.0, 2.0, label="origin")
print(p.x, p.y, p.label)
💡 Single-element tuples need a trailing comma: `(1,)` not `(1)`. Without the comma, Python parses it as a parenthesized expression.

Dictionaries

user = {"name": "Alice", "age": 30, "admin": True}

# Access
print(user["name"])              # KeyError if missing
print(user.get("email", "N/A"))  # safe, returns default

# Modify
user["email"] = "alice@example.com"
user.update({"age": 31, "city": "NYC"})
del user["admin"]

# Iterate
for key in user:
    print(key, user[key])

for key, value in user.items():
    print(f"{key}: {value}")

# Check membership
print("name" in user)   # True (checks keys)

# Dict comprehension
squares = {n: n**2 for n in range(1, 6)}
print(squares)

# Merge dicts (Python 3.9+)
defaults = {"color": "blue", "size": "M"}
overrides = {"color": "red"}
merged = defaults | overrides  # {"color": "red", "size": "M"}
Common Mistake
✗ Avoid
# Accessing a key that might not exist
value = my_dict[key]
✓ Pythonic
value = my_dict.get(key, default_value)
# Or handle explicitly:
try:
    value = my_dict[key]
except KeyError:
    value = default_value

Unguarded dict access raises KeyError. Use .get() or collections.defaultdict.

Sets

# Sets: unordered, unique, fast membership testing
fruits = {"apple", "banana", "cherry", "apple"}
print(fruits)  # {'apple', 'banana', 'cherry'} — deduped

fruits.add("date")
fruits.discard("banana")  # safe — no error if missing

# Set operations
a = {1, 2, 3, 4}
b = {3, 4, 5, 6}

print(a | b)   # union:        {1,2,3,4,5,6}
print(a & b)   # intersection: {3,4}
print(a - b)   # difference:   {1,2}
print(a ^ b)   # symmetric diff: {1,2,5,6}

# Fast deduplication
data = [1, 2, 2, 3, 3, 3, 4]
unique = list(set(data))  # order not preserved!

# Membership test: O(1) vs list O(n)
big_set = set(range(1_000_000))
print(999_999 in big_set)  # instant

if / elif / else

score = 85

if score >= 90:
    grade = "A"
elif score >= 80:
    grade = "B"
elif score >= 70:
    grade = "C"
else:
    grade = "F"

print(grade)  # "B"

# Ternary (conditional expression)
status = "pass" if score >= 60 else "fail"

# Truthy / Falsy values
# Falsy: False, None, 0, 0.0, "", [], (), {}, set()
if not []:
    print("empty list is falsy")

# match statement (Python 3.10+)
command = "quit"
match command:
    case "quit" | "exit":
        print("Goodbye!")
    case "hello":
        print("Hello!")
    case _:
        print(f"Unknown: {command}")

for / while loops

# for loop — iterates over any iterable
for i in range(5):
    print(i)    # 0 1 2 3 4

for i in range(2, 10, 2):
    print(i)    # 2 4 6 8

# enumerate — get index AND value
fruits = ["apple", "banana", "cherry"]
for i, fruit in enumerate(fruits):
    print(i, fruit)

# zip — iterate two lists in parallel
names = ["Alice", "Bob"]
scores = [95, 87]
for name, score in zip(names, scores):
    print(f"{name}: {score}")

# while
count = 0
while count < 5:
    count += 1

# Loop control
for n in range(10):
    if n == 3:
        continue  # skip this iteration
    if n == 7:
        break     # exit loop
    print(n)

# for...else (runs if loop didn't break)
for n in range(5):
    if n == 10:
        break
else:
    print("10 not found")
Common Mistake
✗ Avoid
# Iterating with index manually
for i in range(len(my_list)):
    print(my_list[i])
✓ Pythonic
# Direct iteration
for item in my_list:
    print(item)

# Or with index
for i, item in enumerate(my_list):
    print(i, item)

Direct iteration is more readable, faster, and works with any iterable, not just indexable sequences.

Defining & Calling

def greet(name, greeting="Hello"):
    """Return a greeting string."""
    return f"{greeting}, {name}!"

print(greet("Alice"))             # Hello, Alice!
print(greet("Bob", "Hi"))         # Hi, Bob!
print(greet(greeting="Hey", name="Carol"))  # keyword args

# *args — variable positional arguments
def add(*nums):
    return sum(nums)

print(add(1, 2, 3, 4))   # 10

# **kwargs — variable keyword arguments
def display(**info):
    for key, val in info.items():
        print(f"{key}: {val}")

display(name="Alice", age=30)

# Combining all argument types
def func(pos, /, normal, *, kw_only):
    print(pos, normal, kw_only)

func(1, 2, kw_only=3)   # pos is positional-only

Lambdas, Closures & Higher-Order

# Lambda — anonymous one-liner function
square = lambda x: x ** 2
print(square(5))   # 25

# Used inline with sorted/map/filter
data = [{"name": "Bob", "age": 30}, {"name": "Alice", "age": 25}]
data.sort(key=lambda d: d["age"])

# map and filter
nums = [1, 2, 3, 4, 5]
doubled = list(map(lambda x: x * 2, nums))
evens   = list(filter(lambda x: x % 2 == 0, nums))
# Prefer list comprehensions for these

# Closure — inner function captures outer variable
def make_counter(start=0):
    count = [start]   # mutable container trick not needed with nonlocal
    def increment():
        nonlocal count  # reference outer scope
        count += 1      # won't work — count is int
    # Better:
    def make_adder(n):
        def add(x):
            return x + n
        return add

add5 = make_adder(5)
print(add5(3))   # 8
print(add5(10))  # 15
💡 Prefer list/dict/set comprehensions over `map()` and `filter()` — they read more naturally as English.

Scope (LEGB)

LEGB Rule
x = "global"

def outer():
    x = "enclosing"

    def inner():
        nonlocal x      # refers to enclosing scope
        x = "inner"
        print(x)        # "inner"

    inner()
    print(x)            # "inner" — changed by nonlocal

outer()
print(x)                # "global" — unchanged

# global keyword
counter = 0
def increment():
    global counter
    counter += 1

increment()
print(counter)   # 1
💡 Avoid `global` — it makes functions hard to test. Return values or use class state instead.

List Comprehensions

# [expression for item in iterable if condition]

squares = [x**2 for x in range(1, 11)]
print(squares)

evens = [x for x in range(20) if x % 2 == 0]
print(evens)

# Nested — flatten a 2D list
matrix = [[1,2,3],[4,5,6],[7,8,9]]
flat = [x for row in matrix for x in row]
print(flat)

# With transformation
words = ["hello", "WORLD", "Python"]
normalized = [w.lower().strip() for w in words]
print(normalized)

# Conditional expression inside
labels = ["even" if x % 2 == 0 else "odd" for x in range(6)]
print(labels)
Common Mistake
✗ Avoid
result = []
for x in range(10):
    if x % 2 == 0:
        result.append(x ** 2)
✓ Pythonic
result = [x**2 for x in range(10) if x % 2 == 0]

Comprehensions are compiled to a single bytecode op and run ~35% faster than equivalent for+append loops.

Dict & Set Comprehensions

# Dict comprehension
word = "Mississippi"
freq = {ch: word.count(ch) for ch in set(word)}
print(freq)

# Invert a dict
original = {"a": 1, "b": 2, "c": 3}
inverted = {v: k for k, v in original.items()}
print(inverted)   # {1: 'a', 2: 'b', 3: 'c'}

# Set comprehension
data = [1, 2, 2, 3, 3, 3]
unique_squares = {x**2 for x in data}
print(unique_squares)   # {1, 4, 9}

# Generator expression (lazy — no [] or {})
total = sum(x**2 for x in range(1_000_000))
print(total)   # computed without building a list
💡 Use a generator expression (`sum(x**2 for x in ...)`) instead of a list comprehension when you only need to iterate once — it uses O(1) memory.

try / except / else / finally

def divide(a, b):
    try:
        result = a / b
    except ZeroDivisionError:
        print("Cannot divide by zero")
        return None
    except (TypeError, ValueError) as e:
        print(f"Bad input: {e}")
        return None
    else:
        # Runs ONLY if no exception was raised
        print(f"Success: {result}")
        return result
    finally:
        # ALWAYS runs — cleanup code goes here
        print("divide() completed")

divide(10, 2)
divide(10, 0)
divide("a", 2)
💡 Catch the most specific exception first. Never use bare `except:` — it also catches SystemExit and KeyboardInterrupt, masking real problems.
Common Mistake
✗ Avoid
try:
    result = risky_operation()
except Exception:
    pass   # silent failure
✓ Pythonic
try:
    result = risky_operation()
except SpecificError as e:
    logger.warning("Operation failed: %s", e)
    result = default_value

Silently swallowing exceptions makes bugs invisible. Always log or re-raise.

Custom Exceptions

class AppError(Exception):
    """Base class for application errors."""

class ValidationError(AppError):
    def __init__(self, field, message):
        self.field = field
        self.message = message
        super().__init__(f"{field}: {message}")

class NotFoundError(AppError):
    pass

# Raise and catch
def get_user(user_id):
    if not isinstance(user_id, int):
        raise ValidationError("user_id", "must be an integer")
    if user_id <= 0:
        raise ValidationError("user_id", "must be positive")
    if user_id > 100:
        raise NotFoundError(f"User {user_id} not found")
    return {"id": user_id, "name": "Alice"}

try:
    user = get_user("abc")
except ValidationError as e:
    print(f"Validation failed — {e.field}: {e.message}")
except NotFoundError as e:
    print(f"Not found: {e}")

Context Managers (with)

# with statement — auto-cleanup via __enter__ / __exit__

# File handling (always use with!)
with open("notes.txt", "w") as f:
    f.write("Hello, file!\n")

with open("notes.txt", "r") as f:
    content = f.read()
    print(content)

# Multiple context managers
with open("in.txt") as src, open("out.txt", "w") as dst:
    dst.write(src.read())

# Custom context manager with contextlib
from contextlib import contextmanager

@contextmanager
def timer(label):
    import time
    start = time.perf_counter()
    yield
    elapsed = time.perf_counter() - start
    print(f"{label}: {elapsed:.3f}s")

with timer("my operation"):
    total = sum(range(1_000_000))

Reading & Writing Text

File modes: r (read), w (write/truncate), a (append), b (binary), x (create, fail if exists)
# Write
with open("data.txt", "w", encoding="utf-8") as f:
    f.write("line 1\n")
    f.writelines(["line 2\n", "line 3\n"])

# Read all at once
with open("data.txt", encoding="utf-8") as f:
    content = f.read()

# Read line by line (memory efficient)
with open("data.txt", encoding="utf-8") as f:
    for line in f:
        print(line.rstrip("\n"))

# Read into a list
with open("data.txt", encoding="utf-8") as f:
    lines = f.readlines()   # includes \n

# Append
with open("data.txt", "a", encoding="utf-8") as f:
    f.write("line 4\n")
Common Mistake
✗ Avoid
f = open("data.txt")
content = f.read()
# oops, forgot f.close() — file handle leak
✓ Pythonic
with open("data.txt") as f:
    content = f.read()
# file closed automatically

Always use `with` for file operations. It closes the file even if an exception occurs.

pathlib — Modern Path Handling

pathlib (Python 3.4+)
from pathlib import Path

# Build paths (cross-platform!)
home = Path.home()
project = Path("my_project")
config = project / "config" / "settings.json"

# Info
print(config.name)       # "settings.json"
print(config.stem)       # "settings"
print(config.suffix)     # ".json"
print(config.parent)     # config/

# Create / read / write
project.mkdir(parents=True, exist_ok=True)
config.parent.mkdir(parents=True, exist_ok=True)
config.write_text('{"debug": true}', encoding="utf-8")
data = config.read_text(encoding="utf-8")

# Glob
for py_file in project.rglob("*.py"):
    print(py_file)

# Check existence
if config.exists():
    config.unlink()   # delete
Common Mistake
✗ Avoid
import os
path = os.path.join("project", "data", "file.txt")
✓ Pythonic
from pathlib import Path
path = Path("project") / "data" / "file.txt"

pathlib is object-oriented, cross-platform, and composable. os.path.join is verbose and error-prone.

JSON & CSV

JSON & CSV
import json

# Write JSON
data = {"name": "Alice", "scores": [95, 87, 92]}
with open("data.json", "w") as f:
    json.dump(data, f, indent=2)

# Read JSON
with open("data.json") as f:
    loaded = json.load(f)

# To/from string
s = json.dumps(data)
obj = json.loads(s)

# ─────────────────────────────────
import csv

# Write CSV
rows = [["Name", "Score"], ["Alice", 95], ["Bob", 87]]
with open("scores.csv", "w", newline="") as f:
    writer = csv.writer(f)
    writer.writerows(rows)

# Read CSV as dicts
with open("scores.csv") as f:
    reader = csv.DictReader(f)
    for row in reader:
        print(row["Name"], row["Score"])

Importing

# Import whole module
import math
print(math.sqrt(16))

# Import specific names
from math import sqrt, pi
print(sqrt(16))

# Import with alias
import numpy as np
import pandas as pd

# Import everything (avoid in production)
from math import *

# Relative imports (inside a package)
from . import sibling_module
from ..parent import something

# Conditional import (graceful degradation)
try:
    import ujson as json
except ImportError:
    import json
Common Mistake
✗ Avoid
from module import *   # in production code
✓ Pythonic
import module          # explicit is better
# or
from module import SpecificThing, AnotherThing

Wildcard imports pollute the namespace, shadow existing names, and make it impossible to see where a name comes from.

Package Structure

Package layout
my_package/
├── __init__.py        # makes it a package; can be empty
├── core.py
├── utils.py
└── models/
    ├── __init__.py
    └── user.py

# my_package/__init__.py — control public API
from .core import CoreClass
from .utils import helper_function
__all__ = ["CoreClass", "helper_function"]

# Usage
from my_package import CoreClass   # works because of __init__.py
__name__ guard
# __name__ == "__main__" guard
# Code inside this block only runs when the file is
# executed directly, not when imported as a module.

def main():
    print("Running main logic")

if __name__ == "__main__":
    main()

Classes & Instances

class Animal:
    # Class variable — shared by all instances
    kingdom = "Animalia"

    def __init__(self, name, sound):
        # Instance variables
        self.name = name
        self._sound = sound   # _  = private by convention

    def speak(self):
        return f"{self.name} says {self._sound}!"

    def __repr__(self):
        return f"Animal({self.name!r})"

    def __str__(self):
        return self.name

class Dog(Animal):
    def __init__(self, name):
        super().__init__(name, "Woof")

    def fetch(self):
        return f"{self.name} fetches the ball!"

dog = Dog("Rex")
print(dog.speak())    # Rex says Woof!
print(dog.fetch())
print(repr(dog))      # Animal('Rex')
print(Dog.kingdom)    # Animalia (class var)
print(isinstance(dog, Animal))  # True

Dataclasses (Python 3.7+)

from dataclasses import dataclass, field
from typing import ClassVar

@dataclass
class Point:
    x: float
    y: float
    label: str = ""

    def distance_to(self, other: "Point") -> float:
        return ((self.x - other.x)**2 + (self.y - other.y)**2) ** 0.5

p1 = Point(0, 0, "origin")
p2 = Point(3, 4)
print(p1)              # Point(x=0, y=0, label='origin')
print(p1.distance_to(p2))  # 5.0

# Immutable dataclass
@dataclass(frozen=True)
class Color:
    r: int
    g: int
    b: int

    def hex(self):
        return f"#{self.r:02X}{self.g:02X}{self.b:02X}"

red = Color(255, 0, 0)
print(red.hex())   # #FF0000
💡 Prefer `@dataclass` over a plain class for data containers — you get `__init__`, `__repr__`, and `__eq__` for free.

Properties & Dunder Methods

class Temperature:
    def __init__(self, celsius):
        self._celsius = celsius

    @property
    def celsius(self):
        return self._celsius

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

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

    def __repr__(self):
        return f"Temperature({self._celsius}°C)"

    def __add__(self, other):
        return Temperature(self._celsius + other._celsius)

    def __lt__(self, other):
        return self._celsius < other._celsius

t = Temperature(100)
print(t.fahrenheit)   # 212.0
t.celsius = 0
print(t)              # Temperature(0°C)
t2 = Temperature(25)
print(t < t2)         # True (0 < 25)

Iterator Protocol

# Any object with __iter__ and __next__ is an iterator

class Countdown:
    def __init__(self, start):
        self.current = start

    def __iter__(self):
        return self

    def __next__(self):
        if self.current <= 0:
            raise StopIteration
        val = self.current
        self.current -= 1
        return val

for n in Countdown(5):
    print(n, end=" ")   # 5 4 3 2 1

# iter() / next() builtins
it = iter([10, 20, 30])
print(next(it))   # 10
print(next(it))   # 20
print(next(it, "done"))  # 30
print(next(it, "done"))  # "done" — default, no StopIteration

Generators

# Generator function — use yield instead of return
def fibonacci():
    a, b = 0, 1
    while True:
        yield a
        a, b = b, a + b

fib = fibonacci()
for _ in range(10):
    print(next(fib), end=" ")   # 0 1 1 2 3 5 8 13 21 34

# Finite generator
def squares_up_to(n):
    for x in range(1, n + 1):
        yield x ** 2

print(list(squares_up_to(5)))   # [1, 4, 9, 16, 25]

# Generator expression
big_sum = sum(x**2 for x in range(1_000_000))  # O(1) memory

# yield from — delegate to sub-generator
def chain(*iterables):
    for it in iterables:
        yield from it

print(list(chain([1,2], [3,4], [5])))  # [1,2,3,4,5]
💡 Generators are the Pythonic way to implement lazy evaluation. Use them for infinite sequences, large file parsing, and pipelines that process data item by item.

How Decorators Work

from functools import wraps

# A decorator is a function that takes a function
# and returns a new function.
def log_calls(func):
    @wraps(func)   # preserves __name__, __doc__
    def wrapper(*args, **kwargs):
        print(f"Calling {func.__name__}")
        result = func(*args, **kwargs)
        print(f"{func.__name__} returned {result}")
        return result
    return wrapper

@log_calls
def add(a, b):
    return a + b

add(3, 4)
# Calling add
# add returned 7

# @log_calls is sugar for:
# add = log_calls(add)

Decorator Factories & Class Decorators

from functools import wraps
import time

# Decorator with arguments — needs 3 levels of nesting
def retry(times=3, delay=0.5):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            for attempt in range(1, times + 1):
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    if attempt == times:
                        raise
                    print(f"Attempt {attempt} failed: {e}. Retrying...")
                    time.sleep(delay)
        return wrapper
    return decorator

@retry(times=3, delay=0.1)
def flaky_network_call():
    import random
    if random.random() < 0.7:
        raise ConnectionError("Network down")
    return "success"

# Built-in decorators
class MyClass:
    count = 0

    @classmethod
    def get_count(cls):
        return cls.count

    @staticmethod
    def validate(value):
        return isinstance(value, int) and value > 0

    @property
    def name(self):
        return "MyClass"

Basic Annotations

Type annotations are not enforced at runtime — use mypy or pyright to check statically.
from typing import Optional, Union, Any

# Variables
name: str = "Alice"
count: int = 0
scores: list[float] = []

# Functions
def greet(name: str, times: int = 1) -> str:
    return (f"Hello, {name}! " * times).strip()

# Optional (can be None)
def find_user(user_id: int) -> Optional[dict]:
    ...   # None or a dict

# Python 3.10+ — use X | Y instead of Union
def process(value: int | str | None) -> str:
    return str(value)

# Callable
from typing import Callable
def apply(fn: Callable[[int, int], int], a: int, b: int) -> int:
    return fn(a, b)
Generics
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()
stack.push(1)
stack.push(2)
print(stack.pop())   # 2

TypedDict & Protocol

TypedDict & Protocol
from typing import TypedDict, Protocol

# TypedDict — typed dict structure
class UserDict(TypedDict):
    id: int
    name: str
    email: str

def send_email(user: UserDict) -> None:
    print(f"Sending to {user['email']}")

# Protocol — structural subtyping (duck typing with types)
class Drawable(Protocol):
    def draw(self) -> None: ...

class Circle:
    def draw(self) -> None:
        print("Drawing circle")

class Square:
    def draw(self) -> None:
        print("Drawing square")

def render(shape: Drawable) -> None:
    shape.draw()

# Both Circle and Square satisfy Drawable without inheriting it
render(Circle())
render(Square())

async / await Basics

asyncio basics
import asyncio

async def fetch_data(url: str) -> str:
    # Simulate I/O delay
    await asyncio.sleep(1)
    return f"Data from {url}"

async def main():
    # Run sequentially — total ~2s
    r1 = await fetch_data("https://api.example.com/users")
    r2 = await fetch_data("https://api.example.com/posts")

    # Run concurrently — total ~1s
    r1, r2 = await asyncio.gather(
        fetch_data("https://api.example.com/users"),
        fetch_data("https://api.example.com/posts"),
    )
    print(r1, r2)

asyncio.run(main())
Real I/O with httpx
import asyncio
import httpx   # pip install httpx

async def fetch_all(urls: list[str]) -> list[str]:
    async with httpx.AsyncClient() as client:
        tasks = [client.get(url) for url in urls]
        responses = await asyncio.gather(*tasks)
        return [r.text for r in responses]

# async for / async with
async def stream_lines(path: str):
    import aiofiles   # pip install aiofiles
    async with aiofiles.open(path) as f:
        async for line in f:
            yield line.rstrip()
Common Mistake
✗ Avoid
# Mixing blocking I/O in async code
async def bad():
    import time
    time.sleep(1)      # BLOCKS the event loop!
    import requests
    r = requests.get(url)  # BLOCKS the event loop!
✓ Pythonic
async def good():
    await asyncio.sleep(1)   # yields control
    async with httpx.AsyncClient() as c:
        r = await c.get(url)   # non-blocking

Calling blocking I/O inside an async function stalls the entire event loop. Use async-native libraries.

Threading vs Multiprocessing

GIL note: CPython GIL prevents true parallel threads for CPU work. Use multiprocessing or C extensions for CPU-bound parallelism.
# Threading — best for I/O-bound work (GIL allows concurrent I/O)
from concurrent.futures import ThreadPoolExecutor
import urllib.request

urls = ["https://example.com"] * 5

def fetch(url):
    with urllib.request.urlopen(url) as r:
        return len(r.read())

with ThreadPoolExecutor(max_workers=5) as ex:
    sizes = list(ex.map(fetch, urls))
print(sizes)

# ─────────────────────────────────────────────────────
# Multiprocessing — best for CPU-bound work (bypasses GIL)
from concurrent.futures import ProcessPoolExecutor

def compute(n):
    return sum(i * i for i in range(n))

numbers = [10_000_000] * 4

with ProcessPoolExecutor() as ex:
    results = list(ex.map(compute, numbers))
print(results)
ApproachBest forGIL?Overhead
threadingI/O-bound (network, disk)YesLow
asyncioMany concurrent I/O opsYesVery low
multiprocessingCPU-bound (compute)No (separate process)High
concurrent.futuresBoth (unified API)DependsMedium

Metaclasses & __slots__

__slots__ & Metaclasses
# __slots__ — prevents dynamic attribute creation, saves memory
class Point:
    __slots__ = ("x", "y")

    def __init__(self, x, y):
        self.x = x
        self.y = y

# Can't do: p.z = 3  → AttributeError
# Each instance uses ~40 bytes instead of ~200 bytes with __dict__

# ─────────────────────────────────────────────────────────
# Metaclass — controls class creation
class Singleton(type):
    _instances = {}

    def __call__(cls, *args, **kwargs):
        if cls not in cls._instances:
            cls._instances[cls] = super().__call__(*args, **kwargs)
        return cls._instances[cls]

class Config(metaclass=Singleton):
    def __init__(self):
        self.debug = False

a = Config()
b = Config()
print(a is b)   # True

Descriptors & __getattr__

Descriptors
# Descriptor — object that defines __get__, __set__, or __delete__
class Validated:
    """Descriptor that enforces a type constraint."""
    def __set_name__(self, owner, name):
        self.name = name
        self.private = f"_{name}"

    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        return getattr(obj, self.private, None)

    def __set__(self, obj, value):
        if not isinstance(value, (int, float)):
            raise TypeError(f"{self.name} must be a number")
        setattr(obj, self.private, value)

class Circle:
    radius = Validated()

    def __init__(self, r):
        self.radius = r   # triggers Validated.__set__

c = Circle(5)
print(c.radius)    # 5
c.radius = "big"   # TypeError

Common Pythonic Patterns

Factory, Observer, Strategy
# ── Factory ──────────────────────────────────────────────
class Shape:
    @classmethod
    def create(cls, kind: str) -> "Shape":
        kinds = {"circle": Circle, "square": Square}
        if kind not in kinds:
            raise ValueError(f"Unknown shape: {kind}")
        return kinds[kind]()

# ── Observer ──────────────────────────────────────────────
class EventEmitter:
    def __init__(self):
        self._listeners: dict[str, list] = {}

    def on(self, event: str, fn):
        self._listeners.setdefault(event, []).append(fn)

    def emit(self, event: str, *args):
        for fn in self._listeners.get(event, []):
            fn(*args)

emitter = EventEmitter()
emitter.on("data", lambda d: print(f"Got: {d}"))
emitter.emit("data", 42)

# ── Strategy ──────────────────────────────────────────────
from typing import Callable

def sort_data(data: list, strategy: Callable = sorted) -> list:
    return strategy(data)

print(sort_data([3,1,2]))                    # [1,2,3]
print(sort_data([3,1,2], lambda x: sorted(x, reverse=True)))

pytest Basics

pytest
# pip install pytest pytest-cov
# Run: pytest -v           (verbose)
#      pytest --cov=src    (with coverage)

# test_math.py
def add(a, b):
    return a + b

def test_add():
    assert add(2, 3) == 5
    assert add(-1, 1) == 0

# Parametrize — run one test with many inputs
import pytest

@pytest.mark.parametrize("a, b, expected", [
    (1, 2, 3),
    (0, 0, 0),
    (-1, 1, 0),
    (100, -50, 50),
])
def test_add_parametrized(a, b, expected):
    assert add(a, b) == expected

# Fixtures — reusable setup/teardown
@pytest.fixture
def sample_data():
    return {"users": ["Alice", "Bob"]}

def test_users(sample_data):
    assert "Alice" in sample_data["users"]
Mocking
# Mocking with unittest.mock
from unittest.mock import MagicMock, patch

def test_external_api(mocker):   # pytest-mock
    mock_get = mocker.patch("requests.get")
    mock_get.return_value.json.return_value = {"id": 1}

    from my_module import fetch_user
    user = fetch_user(1)
    assert user["id"] == 1
    mock_get.assert_called_once()

Debugging Tools

Debugging
# pdb — built-in debugger
import pdb; pdb.set_trace()  # old way
breakpoint()                  # Python 3.7+ — respects PYTHONBREAKPOINT env

# Common pdb commands:
# n  → next line
# s  → step into function
# c  → continue
# p var → print var
# l  → list source
# q  → quit

# Logging — better than print()
import logging

logging.basicConfig(
    level=logging.DEBUG,
    format="%(asctime)s %(levelname)s %(name)s — %(message)s",
)
logger = logging.getLogger(__name__)

logger.debug("Processing item %d", item_id)
logger.info("User %s logged in", username)
logger.warning("Cache miss for key: %s", key)
logger.error("Failed to connect: %s", err)
logger.exception("Unhandled error")   # includes traceback
Profiling
# Profiling
import cProfile
import pstats

cProfile.run("my_function()", "output.prof")
stats = pstats.Stats("output.prof")
stats.sort_stats("cumulative")
stats.print_stats(10)   # top 10

# Quick timing
import timeit
time = timeit.timeit("sum(range(1000))", number=10_000)
print(f"{time:.3f}s for 10,000 iterations")

# memory_profiler — pip install memory-profiler
from memory_profiler import profile

@profile
def memory_heavy():
    data = [i for i in range(1_000_000)]
    return sum(data)

Essential Modules

collections & itertools
# collections — specialized data structures
from collections import Counter, defaultdict, deque, OrderedDict

text = "to be or not to be that is the question"
freq = Counter(text.split())
print(freq.most_common(3))   # [('be', 2), ('to', 2), ('or', 1)]

# defaultdict — no KeyError for missing keys
graph = defaultdict(list)
graph["A"].append("B")

# deque — O(1) append/pop from both ends
queue = deque()
queue.appendleft("first")
queue.append("last")
queue.popleft()   # O(1) vs list.pop(0) which is O(n)

# ─────────────────────────────────────────────────────
# itertools — combinatoric tools
import itertools

print(list(itertools.chain([1,2], [3,4])))   # [1,2,3,4]
print(list(itertools.product("AB", repeat=2)))  # cartesian product
print(list(itertools.combinations("ABCD", 2)))  # C(4,2)
print(list(itertools.permutations("AB")))

# groupby — group consecutive elements
data = [("A",1),("A",2),("B",3),("B",4)]
for key, group in itertools.groupby(data, key=lambda x: x[0]):
    print(key, list(group))
functools, datetime, subprocess
# functools
from functools import lru_cache, partial, reduce

@lru_cache(maxsize=None)
def fib(n):
    if n < 2: return n
    return fib(n-1) + fib(n-2)

print(fib(100))   # instant with caching

double = partial(lambda x, n: x * n, n=2)

# datetime
from datetime import datetime, timedelta, timezone

now = datetime.now(tz=timezone.utc)
fmt = now.strftime("%Y-%m-%d %H:%M:%S UTC")
parsed = datetime.fromisoformat("2026-05-16T12:00:00+00:00")
delta = timedelta(days=30)
future = now + delta

# os & subprocess
import os, subprocess
print(os.environ.get("HOME", "unknown"))
result = subprocess.run(["echo", "hello"], capture_output=True, text=True)
print(result.stdout.strip())   # hello

Essential Tools

ToolPurposeInstall
ruffLinter + formatter (replaces flake8, isort, black)pip install ruff
mypyStatic type checkerpip install mypy
pyrightType checker (faster, VS Code default)pip install pyright
pytestTesting frameworkpip install pytest
uvFast package manager (replaces pip+venv)pip install uv
pre-commitRun checks before git commitpip install pre-commit
ipythonBetter REPL with tab completionpip install ipython
richBeautiful terminal outputpip install rich
pyproject.toml
# pyproject.toml — modern project config
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "my-project"
version = "0.1.0"
requires-python = ">=3.10"
dependencies = ["requests>=2.31", "pydantic>=2.0"]

[project.optional-dependencies]
dev = ["pytest", "ruff", "mypy"]

[tool.ruff]
line-length = 88
target-version = "py310"

[tool.ruff.lint]
select = ["E", "F", "I", "UP"]

[tool.mypy]
strict = true
python_version = "3.10"

Write Python, not Java

# EAFP — Easier to Ask Forgiveness than Permission
# (Pythonic) — try and catch exceptions
try:
    value = my_dict[key]
except KeyError:
    value = default

# vs LBYL — Look Before You Leap (non-Pythonic)
if key in my_dict:
    value = my_dict[key]
else:
    value = default

# ─── Unpacking everywhere ─────────────────────────────────
first, *rest = [1, 2, 3, 4, 5]
print(first)  # 1
print(rest)   # [2, 3, 4, 5]

a, *_, z = range(10)
print(a, z)   # 0 9

# ─── any() / all() ────────────────────────────────────────
nums = [2, 4, 6, 8]
print(all(n % 2 == 0 for n in nums))   # True — all even
print(any(n > 7 for n in nums))        # True — some > 7

# ─── zip with strict (Python 3.10+) ──────────────────────
for a, b in zip([1,2,3], [4,5,6], strict=True):
    print(a, b)   # raises if lengths differ
Common Mistake
✗ Avoid
# Java-style getter/setter
class Person:
    def __init__(self, name):
        self._name = name
    def get_name(self):
        return self._name
    def set_name(self, name):
        self._name = name
✓ Pythonic
class Person:
    def __init__(self, name):
        self.name = name   # public by default

    @property
    def display_name(self):   # only add property if you need logic
        return self.name.title()

In Python, attributes are public by default. Add @property only when you need to validate or compute.

Gotchas to Know

Common Mistake
✗ Avoid
# Mutable default argument
def append_to(elem, to=[]):
    to.append(elem)
    return to

print(append_to(1))  # [1]
print(append_to(2))  # [1, 2]  ← !! list persists!
✓ Pythonic
def append_to(elem, to=None):
    if to is None:
        to = []
    to.append(elem)
    return to

Default argument values are evaluated ONCE at function definition time — not each call.

Common Mistake
✗ Avoid
# Late binding closure
fns = [lambda: i for i in range(5)]
print([f() for f in fns])  # [4, 4, 4, 4, 4] ← all 4!
✓ Pythonic
fns = [lambda i=i: i for i in range(5)]
print([f() for f in fns])  # [0, 1, 2, 3, 4]

Lambda captures variable by reference, not by value. Use default argument to capture the current value.

Common Mistake
✗ Avoid
# Comparing with == instead of is for None/True/False
if x == None:   # wrong
    ...
✓ Pythonic
if x is None:   # correct — singletons use identity check
    ...
if x is True:   # or just: if x:
    ...

`None`, `True`, and `False` are singletons. Use `is` for identity comparison.

Common Mistake
✗ Avoid
# Modifying a list while iterating over it
for item in my_list:
    if condition(item):
        my_list.remove(item)   # skips items!
✓ Pythonic
# Iterate over a copy
for item in my_list[:]:
    if condition(item):
        my_list.remove(item)

# Or use a comprehension
my_list = [item for item in my_list if not condition(item)]

Removing items from a list while iterating changes the list length and causes items to be skipped.

Common Mistake
✗ Avoid
# Using + to build strings in a loop
result = ""
for s in many_strings:
    result += s   # O(n²) — new string allocated each time
✓ Pythonic
result = "".join(many_strings)   # O(n)

String concatenation with += in a loop is O(n²). Use str.join() for O(n) performance.

Common Mistake
✗ Avoid
# Catching the base Exception too broadly
try:
    result = compute()
except Exception:
    pass   # KeyboardInterrupt, MemoryError etc. slip through
✓ Pythonic
try:
    result = compute()
except (ValueError, TypeError) as e:
    logger.error("compute failed: %s", e)
    result = fallback()

Catch the most specific exception. Never silence exceptions silently.

References & Learning

ResourceURLBest for
Official Docsdocs.python.org/3Definitive reference
PEP 8peps.python.org/pep-0008Style guide
Real Pythonrealpython.comIn-depth tutorials
PyMOTWpymotw.comStandard library examples
Awesome Pythongithub.com/vinta/awesome-pythonLibrary directory
Python Cookbookoreilly.com/library/view/python-cookbookRecipes & patterns
Built-inDescription
len(x)Length of sequence/container
range(n)Integer sequence 0..n-1
enumerate(it)Index-value pairs
zip(*its)Parallel iteration
map(fn, it)Apply function to each item
filter(fn, it)Keep items where fn is True
sorted(it)Return sorted list
reversed(seq)Reversed iterator
sum(it)Sum of numbers
min/max(it)Minimum/maximum value
any/all(it)Boolean over iterable
isinstance(x, T)Type check
getattr(obj, name)Dynamic attribute access
vars(obj)Object's __dict__
dir(obj)All attributes & methods
help(obj)Built-in documentation
💡 Run `python -m this` in your terminal to read the Zen of Python — the philosophy that guides every design decision in the language.

Frequently Asked Questions

Is this Python cheat sheet free?

Yes — completely free, forever, with no signup required. Every feature including the live code runner, search, and dark mode works without an account. There are no paywalled sections.

Can I run the code examples in my browser?

Yes. Click the Run button on any code snippet to launch the browser-based Python runtime (powered by Pyodide). The runtime loads on first use (~5–10 seconds on a typical connection) and then all subsequent runs are instant. You can edit the code before running it.

Which version of Python does this cheat sheet cover?

This cheat sheet targets Python 3.10+ as the baseline, with notes on Python 3.12 and 3.13 features. Structural pattern matching (match/case) requires 3.10+. The new X | Y union type syntax for type hints requires 3.10+. Most examples work on Python 3.8+.

Can I use this cheat sheet during a live stream or in a video?

Yes — it includes a Stream Mode toggle designed specifically for YouTube and Twitch live coding. Stream Mode increases font sizes, collapses the navigation sidebar to icon-only, and hides ads to keep the screen clean. The site owner uses this page in their own live streams.

How do I search the cheat sheet quickly?

Press Cmd+K (Mac) or Ctrl+K (Windows) to open the search modal, or press the / key anywhere on the page. Search covers section titles, subsection titles, concept keywords, and code snippet content. Press Esc to close, Arrow keys to navigate results, Enter to jump.

Will this work offline?

The cheat sheet page and all content work offline once initially loaded — the page is fully static with no server dependency. The live code runner requires a one-time download of the Pyodide Python runtime (~10 MB). After that initial download (which browsers cache), code execution works offline.