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
Install & Run
Python 3.10+ is recommended. Download from python.org or use a version manager.
# Check version
python --version # or python3 --version
# Interactive REPL
python
# Run a script
python hello.py
# Run a module
python -m http.server 8080# 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 requestsHello World & Conventions
print("Hello, World!")
print("Python", 3.12, "rocks")
name = "Alice"
print(f"Hello, {name}!")| Convention | Example | Used for |
|---|---|---|
| snake_case | user_name, get_data() | Variables, functions, modules |
| PascalCase | UserProfile, HttpClient | Classes |
| SCREAMING_SNAKE | MAX_RETRIES, API_URL | Constants |
| _single_leading | _internal_method() | Private by convention |
| __double_leading | __slots__ | Name-mangled class attrs |
Virtual Environments
# 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# Installing packages globally
pip install flask# Always use a virtual environment
python -m venv .venv && source .venv/bin/activate
pip install flaskGlobal 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| Type | Literal example | Mutable? |
|---|---|---|
| int | 42, -7, 0 | No |
| float | 3.14, 1e10, float("inf") | No |
| complex | 2+3j | No |
| str | "hello", 'world' | No |
| bool | True, False | No |
| NoneType | None | No |
| list | [1, 2, 3] | Yes |
| tuple | (1, 2, 3) | No |
| dict | {"a": 1} | Yes |
| set | {1, 2, 3} | Yes |
| frozenset | frozenset({1, 2}) | No |
| bytes | b"hello" | No |
| bytearray | bytearray(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.00.1 + 0.2 == 0.3 # False — floating point!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")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"msg = "Hello, " + name + "! You are " + str(age) + "."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----"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"))# Checking if list is empty
if len(my_list) == 0:
print("empty")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)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"}# Accessing a key that might not exist
value = my_dict[key]value = my_dict.get(key, default_value)
# Or handle explicitly:
try:
value = my_dict[key]
except KeyError:
value = default_valueUnguarded 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) # instantif / 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")# Iterating with index manually
for i in range(len(my_list)):
print(my_list[i])# 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-onlyLambdas, 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)) # 15Scope (LEGB)
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) # 1List 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)result = []
for x in range(10):
if x % 2 == 0:
result.append(x ** 2)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 listtry / 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)try:
result = risky_operation()
except Exception:
pass # silent failuretry:
result = risky_operation()
except SpecificError as e:
logger.warning("Operation failed: %s", e)
result = default_valueSilently 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
# 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")f = open("data.txt")
content = f.read()
# oops, forgot f.close() — file handle leakwith open("data.txt") as f:
content = f.read()
# file closed automaticallyAlways use `with` for file operations. It closes the file even if an exception occurs.
pathlib — Modern Path Handling
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() # deleteimport os
path = os.path.join("project", "data", "file.txt")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
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 jsonfrom module import * # in production codeimport module # explicit is better
# or
from module import SpecificThing, AnotherThingWildcard imports pollute the namespace, shadow existing names, and make it impossible to see where a name comes from.
Package Structure
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__ == "__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)) # TrueDataclasses (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()) # #FF0000Properties & 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 StopIterationGenerators
# 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]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
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)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()) # 2TypedDict & 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
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())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()# 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!async def good():
await asyncio.sleep(1) # yields control
async with httpx.AsyncClient() as c:
r = await c.get(url) # non-blockingCalling blocking I/O inside an async function stalls the entire event loop. Use async-native libraries.
Threading vs Multiprocessing
# 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)| Approach | Best for | GIL? | Overhead |
|---|---|---|---|
| threading | I/O-bound (network, disk) | Yes | Low |
| asyncio | Many concurrent I/O ops | Yes | Very low |
| multiprocessing | CPU-bound (compute) | No (separate process) | High |
| concurrent.futures | Both (unified API) | Depends | Medium |
Metaclasses & __slots__
# __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) # TrueDescriptors & __getattr__
# 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" # TypeErrorCommon Pythonic Patterns
# ── 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
# 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 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
# 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
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 — 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
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()) # helloHTTP: requests & httpx
# pip install requests httpx
import requests
# Synchronous
r = requests.get("https://api.github.com/users/octocat")
r.raise_for_status() # raises on 4xx/5xx
data = r.json() # parsed JSON
print(data["name"])
# POST with JSON body
resp = requests.post(
"https://httpbin.org/post",
json={"key": "value"},
headers={"Authorization": "Bearer token"},
timeout=5,
)
# Sessions — reuse connection, persist cookies
with requests.Session() as s:
s.headers["Authorization"] = "Bearer token"
r = s.get("https://api.example.com/data")
# ─────────────────────────────────────────────────────
# httpx — modern, async-native
import httpx
with httpx.Client() as c:
r = c.get("https://httpbin.org/get")
print(r.json())Data Science Stack (quick ref)
# numpy — numerical arrays
import numpy as np
arr = np.array([1, 2, 3, 4, 5])
print(arr * 2) # [2, 4, 6, 8, 10]
print(arr.mean()) # 3.0
matrix = np.zeros((3, 3))
identity = np.eye(3)
# pandas — tabular data
import pandas as pd
df = pd.read_csv("data.csv")
print(df.head())
print(df.describe())
filtered = df[df["age"] > 30]
grouped = df.groupby("city")["salary"].mean()
# Pydantic — data validation (pip install pydantic)
from pydantic import BaseModel, EmailStr
class User(BaseModel):
id: int
name: str
email: str
age: int
user = User(id=1, name="Alice", email="alice@example.com", age=30)
print(user.model_dump())
# FastAPI uses Pydantic for request/response modelsEssential Tools
| Tool | Purpose | Install |
|---|---|---|
| ruff | Linter + formatter (replaces flake8, isort, black) | pip install ruff |
| mypy | Static type checker | pip install mypy |
| pyright | Type checker (faster, VS Code default) | pip install pyright |
| pytest | Testing framework | pip install pytest |
| uv | Fast package manager (replaces pip+venv) | pip install uv |
| pre-commit | Run checks before git commit | pip install pre-commit |
| ipython | Better REPL with tab completion | pip install ipython |
| rich | Beautiful terminal output | pip install rich |
# 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# 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 = nameclass 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
# 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!def append_to(elem, to=None):
if to is None:
to = []
to.append(elem)
return toDefault argument values are evaluated ONCE at function definition time — not each call.
# Late binding closure
fns = [lambda: i for i in range(5)]
print([f() for f in fns]) # [4, 4, 4, 4, 4] ← all 4!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.
# Comparing with == instead of is for None/True/False
if x == None: # wrong
...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.
# Modifying a list while iterating over it
for item in my_list:
if condition(item):
my_list.remove(item) # skips items!# 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.
# Using + to build strings in a loop
result = ""
for s in many_strings:
result += s # O(n²) — new string allocated each timeresult = "".join(many_strings) # O(n)String concatenation with += in a loop is O(n²). Use str.join() for O(n) performance.
# Catching the base Exception too broadly
try:
result = compute()
except Exception:
pass # KeyboardInterrupt, MemoryError etc. slip throughtry:
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
| Resource | URL | Best for |
|---|---|---|
| Official Docs | docs.python.org/3 | Definitive reference |
| PEP 8 | peps.python.org/pep-0008 | Style guide |
| Real Python | realpython.com | In-depth tutorials |
| PyMOTW | pymotw.com | Standard library examples |
| Awesome Python | github.com/vinta/awesome-python | Library directory |
| Python Cookbook | oreilly.com/library/view/python-cookbook | Recipes & patterns |
| Built-in | Description |
|---|---|
| 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 |