Python rewards developers who know its core syntax, built-in data structures, and idiomatic patterns by heart. This cheat sheet condenses all three into one reference. It covers variables, control flow, comprehensions, functions, classes, and the tools that separate readable Python from fragile scripts. Every snippet runs on Python 3.10+ and follows PEP 8.
Quick Takeaways
- Lists are ordered and mutable, tuples are ordered and immutable, sets are unordered and unique, dicts map keys to values in insertion order.
- List comprehensions replace most
forloops that build collections, and run faster than manual.append()loops. - Use
dataclasses, type hints, and context managers (with) to write production-grade code with less boilerplate. - Pick the structure by access pattern: O(1) lookups need a
dictorset, not alist.
| Structure | Syntax | Ordered | Mutable | Duplicates | Lookup |
|---|---|---|---|---|---|
| list | [1, 2, 3] |
Yes | Yes | Yes | O(n) |
| tuple | (1, 2, 3) |
Yes | No | Yes | O(n) |
| set | {1, 2, 3} |
No | Yes | No | O(1) |
| dict | {"a": 1} |
Yes (3.7+) | Yes | Unique keys | O(1) |
| frozenset | frozenset({1, 2}) |
No | No | No | O(1) |
Core Syntax Breakdown
Variables and Data Types
Python is dynamically typed. A variable is a name bound to an object, not a typed memory slot.
name = "Ada" # str
age = 36 # int
height = 1.72 # float
is_active = True # bool
nothing = None # NoneType
# Multiple assignment and swapping
x, y = 10, 20
x, y = y, x # No temp variable needed
After the swap, x is 20 and y is 10. Python builds a tuple on the right-hand side, then unpacks it.
Type Hints
Type hints do not enforce types at runtime. Tools like mypy and pyright use them to catch bugs before execution.
def greet(name: str, times: int = 1) -> str:
"""Return a repeated greeting."""
return " ".join(f"Hello, {name}!" for _ in range(times))
print(greet("Ada", 2))
Output: Hello, Ada! Hello, Ada!
Strings and f-Strings
language = "Python"
version = 3.13
print(f"{language} {version}") # Python 3.13
print(f"{version:.1f}") # 3.1 (format spec)
print(f"{language!r}") # 'Python' (repr)
print(f"{1_000_000:,}") # 1,000,000
print(f"{language=}") # language='Python' (debug)
Common string methods:
| Method | Purpose | Example |
|---|---|---|
.strip() |
Trim whitespace | " hi ".strip() → "hi" |
.split() |
String to list | "a,b".split(",") → ["a", "b"] |
.join() |
List to string | "-".join(["a", "b"]) → "a-b" |
.replace() |
Substitute | "cat".replace("c", "b") → "bat" |
.startswith() |
Prefix check | "python".startswith("py") → True |
Control Flow
Conditionals and Pattern Matching
score = 82
if score >= 90:
grade = "A"
elif score >= 80:
grade = "B"
else:
grade = "C"
# Ternary expression
status = "pass" if score >= 60 else "fail"
Structural pattern matching (match/case, Python 3.10+) handles shape-based branching:
def handle(event: dict) -> str:
match event:
case {"type": "click", "x": x, "y": y}:
return f"Click at ({x}, {y})"
case {"type": "key", "key": "q"}:
return "Quit"
case {"type": "key", "key": key}:
return f"Key: {key}"
case _:
return "Unknown"
print(handle({"type": "click", "x": 4, "y": 9}))
Output: Click at (4, 9). The first matching case wins, and _ is the wildcard fallback.
Loops
fruits = ["apple", "banana", "cherry"]
# enumerate gives index and value
for i, fruit in enumerate(fruits, start=1):
print(i, fruit)
# zip pairs iterables
prices = [1.2, 0.5, 3.0]
for fruit, price in zip(fruits, prices, strict=True):
print(f"{fruit}: ${price}")
The strict=True flag (3.10+) raises ValueError if the iterables differ in length. Use it to catch silent data loss.
Data Structures in Depth
Lists
nums = [5, 2, 8, 1]
nums.append(9) # add to end: O(1)
nums.insert(0, 7) # add at index: O(n)
nums.pop() # remove last: O(1)
nums.sort() # in place: O(n log n)
sorted_desc = sorted(nums, reverse=True) # new list
print(nums[1:3]) # slice -> [2, 5]
print(nums[::-1]) # reversed copy
Tuples and Unpacking
point = (3, 4)
x, y = point
first, *rest = [1, 2, 3, 4]
print(first, rest) # 1 [2, 3, 4]
Tuples are hashable when their contents are hashable, so they work as dict keys.
Dictionaries
user = {"name": "Ada", "role": "admin"}
user["email"] = "ada@example.com"
print(user.get("phone", "N/A")) # safe access with default
for key, value in user.items():
print(key, value)
# Merge (3.9+)
defaults = {"theme": "dark", "lang": "en"}
config = defaults | {"lang": "fr"}
print(config) # {'theme': 'dark', 'lang': 'fr'}
Use .get() instead of [] when a key may be missing. It avoids KeyError.
Sets
a = {1, 2, 3, 4}
b = {3, 4, 5}
print(a | b) # union: {1, 2, 3, 4, 5}
print(a & b) # intersection: {3, 4}
print(a - b) # difference: {1, 2}
print(a ^ b) # symmetric difference: {1, 2, 5}
# Deduplicate while losing order
unique = list(set([1, 2, 2, 3]))
# Deduplicate while keeping order
ordered_unique = list(dict.fromkeys([3, 1, 3, 2, 1]))
The collections Module
from collections import Counter, defaultdict, deque
# Count occurrences
print(Counter("mississippi").most_common(2)) # [('i', 4), ('s', 4)]
# Auto-initialize missing keys
groups = defaultdict(list)
for word in ["apple", "avocado", "banana"]:
groups[word[0]].append(word)
print(dict(groups)) # {'a': ['apple', 'avocado'], 'b': ['banana']}
# O(1) appends and pops from both ends
queue = deque([1, 2, 3])
queue.appendleft(0)
queue.pop()
Use deque for queues. list.pop(0) costs O(n), while deque.popleft() costs O(1).
Comprehensions and Generators
# List comprehension
squares = [n**2 for n in range(10) if n % 2 == 0]
# Dict comprehension
lengths = {word: len(word) for word in ["a", "bb", "ccc"]}
# Set comprehension
initials = {w[0] for w in ["apple", "avocado", "banana"]}
# Generator expression: lazy, O(1) memory
total = sum(n**2 for n in range(1_000_000))
A comprehension in brackets builds the entire list in memory. A generator expression in parentheses yields one item at a time. Prefer generators when you only iterate once.
Functions
def connect(host: str, *, port: int = 5432, timeout: float = 5.0) -> str:
"""Keyword-only args after the bare `*`."""
return f"{host}:{port} (timeout={timeout}s)"
def total(*args: int, **kwargs: int) -> int:
"""*args collects positionals; **kwargs collects keywords."""
return sum(args) + sum(kwargs.values())
print(connect("db.local", port=6543))
print(total(1, 2, bonus=10)) # 13
Mutable Default Argument Trap
# Bad: the list is created once and shared across calls
def add_item(item, bucket=[]):
bucket.append(item)
return bucket
# Good: use None as a sentinel
def add_item_safe(item, bucket=None):
if bucket is None:
bucket = []
bucket.append(item)
return bucket
Decorators
import functools
import time
def timed(func):
@functools.wraps(func) # preserves __name__ and docstring
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
elapsed = time.perf_counter() - start
print(f"{func.__name__} took {elapsed:.4f}s")
return result
return wrapper
@timed
def slow_sum(n: int) -> int:
return sum(range(n))
slow_sum(5_000_000)
The decorator prints the elapsed time, then returns the original result. Always apply functools.wraps so introspection and debugging still work.
Classes and Dataclasses
from dataclasses import dataclass, field
@dataclass(slots=True, frozen=True)
class Point:
x: float
y: float
def distance_to(self, other: "Point") -> float:
return ((self.x - other.x) ** 2 + (self.y - other.y) ** 2) ** 0.5
@dataclass
class Order:
id: int
items: list[str] = field(default_factory=list)
p1, p2 = Point(0, 0), Point(3, 4)
print(p1.distance_to(p2)) # 5.0
@dataclass generates __init__, __repr__, and __eq__ automatically. frozen=True makes instances immutable and hashable. slots=True cuts per-instance memory overhead.
Error Handling and Context Managers
from pathlib import Path
def read_config(path: str) -> str:
try:
return Path(path).read_text(encoding="utf-8")
except FileNotFoundError:
return ""
except PermissionError as exc:
raise RuntimeError(f"Cannot read {path}") from exc
else:
pass # runs only if no exception occurred
finally:
print("Done") # always runs
Custom context manager:
from contextlib import contextmanager
@contextmanager
def opened(path: str):
f = open(path, encoding="utf-8")
try:
yield f
finally:
f.close()
with opened("notes.txt") as f:
print(f.readline())
Performance Comparison: Choosing the Right Tool
| Task | Slow Choice | Fast Choice | Complexity Gain |
|---|---|---|---|
| Membership test | x in list |
x in set |
O(n) → O(1) |
| Queue pop from front | list.pop(0) |
deque.popleft() |
O(n) → O(1) |
| String building | s += "x" in loop |
"".join(parts) |
O(n²) → O(n) |
| Counting items | Manual dict loop | Counter(items) |
Cleaner and C-optimized |
| Sum of large range | List then sum() |
Generator in sum() |
O(n) memory → O(1) |
Best Practices: Anti-Patterns vs. Refactored Code
Anti-Pattern 1: Manual Index Loops
# Bad
names = ["Ada", "Linus", "Guido"]
for i in range(len(names)):
print(i, names[i])
# Good
for i, name in enumerate(names):
print(i, name)
Anti-Pattern 2: Bare except
# Bad: hides bugs and swallows KeyboardInterrupt
try:
risky()
except:
pass
# Good: catch specific errors and log them
import logging
try:
risky()
except ValueError as exc:
logging.warning("Invalid value: %s", exc)
Anti-Pattern 3: Building Strings in a Loop
# Bad: creates a new string each iteration
result = ""
for word in words:
result += word + " "
# Good: a single allocation
result = " ".join(words)
Anti-Pattern 4: Opening Files Without with
# Bad: file may stay open if an error occurs
f = open("data.txt")
data = f.read()
f.close()
# Good: automatic cleanup
with open("data.txt", encoding="utf-8") as f:
data = f.read()
Anti-Pattern 5: Checking Type with ==
# Bad
if type(value) == list:
...
# Good: respects inheritance
if isinstance(value, list):
...
Useful Standard Library Patterns
from itertools import chain, groupby, islice
from functools import lru_cache, partial
from pathlib import Path
import json
# Memoization
@lru_cache(maxsize=None)
def fib(n: int) -> int:
return n if n < 2 else fib(n - 1) + fib(n - 2)
print(fib(50)) # 12586269025, instant thanks to caching
# Flatten nested lists
nested = [[1, 2], [3], [4, 5]]
print(list(chain.from_iterable(nested))) # [1, 2, 3, 4, 5]
# Read and write JSON with pathlib
data = {"name": "Ada", "langs": ["Python"]}
Path("user.json").write_text(json.dumps(data, indent=2), encoding="utf-8")
loaded = json.loads(Path("user.json").read_text(encoding="utf-8"))
@lru_cache turns exponential recursion into linear time by storing results. pathlib.Path replaces most os.path string juggling with readable objects.
Async Basics
import asyncio
async def fetch(name: str, delay: float) -> str:
await asyncio.sleep(delay) # non-blocking wait
return f"{name} done"
async def main() -> None:
results = await asyncio.gather(
fetch("A", 1.0),
fetch("B", 1.0),
)
print(results)
asyncio.run(main())
Both tasks run concurrently, so the total runtime is about 1 second, not 2. Use asyncio for I/O-bound work such as HTTP calls and database queries. Use multiprocessing for CPU-bound work.
Quick Reference: Operators and Built-ins
| Category | Items |
|---|---|
| Arithmetic | + - * / // % ** |
| Comparison | == != < > <= >= |
| Logical | and or not |
| Identity / Membership | is, is not, in, not in |
| Useful built-ins | len(), sum(), min(), max(), any(), all(), sorted(), reversed(), map(), filter() |
Note that / always returns a float, while // returns the floor-divided integer.
FAQ
What is the difference between a list and a tuple in Python?
A list is mutable, so you can add, remove, or change elements. A tuple is immutable and can serve as a dict key when its elements are hashable. Use tuples for fixed records like coordinates, and lists for collections that change.
When should I use a list comprehension instead of a for loop?
Use a comprehension when you build a new collection from one iterable with a simple transformation or filter. Switch to a regular for loop when the logic needs multiple statements, side effects, or nested conditions that hurt readability.
What is the difference between is and == in Python?
== compares values for equality. is compares object identity, meaning whether both names point to the same object in memory. Use is only for singletons like None, as in if x is None.
How do I avoid the mutable default argument bug?
Set the default to None and create the mutable object inside the function body. Default values are evaluated once at function definition time, so a default [] or {} is shared across every call.




