Python is a general-purpose language that reads almost like plain English, which is
why it is the usual first language and the usual choice for scripts, data work and
automation. The reference below is grouped by what you are trying to do, and the
filter box searches all of it at once. Type dict and everything about
dictionaries comes to you, or type 3.14 to see what the latest release added.
Every snippet is checked against Python 3.14, the current version, and was run
on the 3.14.7 interpreter. Anything newer than 3.8 says so in the notes column.
Names like user and nums are placeholders for your own. Coming from another
language? The Ruby,
PHP and Lua cheat
sheets are grouped the same way, so they read side by side.
Searches the task, the command and the third column. Press / from anywhere on the page.
307 commands
Running Python
| Task | Command | Notes |
|---|---|---|
| Check which version you have | python3 --version | On Windows, py --version |
| Run a script | python3 app.py | |
| Open an interactive shell | python3 | Leave with exit() or Ctrl+D (Ctrl+Z then Enter on Windows) |
| Run a script, then stay in the shell | python3 -i app.py | Every variable from the script is still there |
| Run one line without a file | python3 -c 'print(2 ** 100)' | |
| Run a module as a program | python3 -m http.server 8000 | Serves the current folder on localhost:8000 |
| Pretty-print a JSON file | python3 -m json data.json | 3.14+. python3 -m json.tool on older versions |
| Time a small piece of code | python3 -m timeit "sum(range(1000))" | |
| Check a file for syntax errors | python3 -m py_compile app.py | |
| Run the tests in a folder | python3 -m unittest discover | |
| Only run code when the file is run directly | if __name__ == "__main__": | Not when another file imports it |
| Make a script executable on Linux or macOS | #!/usr/bin/env python3 | First line of the file, then chmod +x app.py |
| Read the docs for anything | help(str.split) | In the shell. dir(obj) lists its attributes |
On macOS and Linux the command is usually python3, because python can be missing or, on older systems, Python 2. On Windows, the py launcher from python.org picks the newest Python you have installed.
Variables and types
| Task | Code | Notes |
|---|---|---|
| Print a line | print("Hello") | Several arguments are separated by a space |
| Print without a newline | print("Loading", end="") | |
| Assign a variable | name = "Ada" | No keyword and no declared type. snake_case by convention |
| Constant, by convention | MAX_USERS = 100 | Capitals say do not change it. Nothing stops you |
| Assign several at once | a, b = 1, 2 | |
| Swap two variables | a, b = b, a | |
| Comment | # one line | No block comment. Use # on each line |
| What type is it | type(value) | type(3) is <class 'int'> |
| Is it a kind of | isinstance(value, int) | True for subclasses too, so isinstance(True, int) is True |
| Is it one of several types | isinstance(value, int | float) | 3.10+. A tuple (int, float) works everywhere |
| No value | result = None | |
| Is it None | value is None | Use is, not == |
| Numbers | 42 3.14 1_000_000 2e3 | Underscores are ignored. Integers never overflow |
| Divide | 7 / 2 | Always a float: 3.5 |
| Floor division | 7 // 2 | 3. Rounds down, so -7 // 2 is -4 |
| Remainder | 7 % 3 | Takes the sign of the divisor: -7 % 3 is 2 |
| Both at once | divmod(17, 5) | (3, 2) |
| Raise to a power | 2 ** 10 | 1024. Not ^, which is bitwise xor |
| Round | round(3.14159, 2) | Halves go to the even number: round(2.5) is 2 |
| String to number | int("42") float("3.5") | ValueError when it is not a number |
| Parse in another base | int("ff", 16) | 255 |
| Number to string | str(42) | |
| True and false | True False | Capitalised |
| Default when falsy | name = user_input or "Anonymous" | Also replaces 0 and empty strings |
| Pick one of two values | label = "item" if n == 1 else "items" | |
| Chain comparisons | 0 <= x < 10 | |
| Assign inside an expression | if (n := len(items)) > 10: | The walrus operator. 3.8+ |
| Type hint | count: int = 0 | Not checked when the code runs. Tools like mypy read it |
| Name a type | type UserId = int | 3.12+. UserId: TypeAlias = int before that |
| Exact decimals, for money | from decimal import Decimal Decimal("0.1") + Decimal("0.2") | Decimal('0.3'). Pass strings, not floats |
Falsy values are None, False, 0, 0.0, the empty string and every empty collection: [], {}, set() and (). Everything else is truthy, so if items: is the usual way to ask whether a list has anything in it.
Strings and f-strings
Strings cannot be changed in place. Every method returns a new string, so assign the result: s = s.strip(). Positions start at 0, and negative positions count from the end.
| Task | Code | Notes |
|---|---|---|
| Single or double quotes | 'cat' "cat" | The same. Pick one and stick to it |
| Put a value in a string | f"Hello, {name}" | An f-string. Any expression goes inside the braces |
| Two decimal places | f"{price:.2f}" | 3.14159 becomes 3.14 |
| Thousands separator | f"{n:,}" | 1234567 becomes 1,234,567 |
| Percentage | f"{ratio:.1%}" | 0.256 becomes 25.6% |
| Pad and align | f"{name:<10}" f"{name:>10}" f"{name:^10}" | Left, right, centre in 10 characters |
| Pad a number with zeros | f"{7:03}" | 007 |
| Show the name and the value | f"{total=}" | total=42. For debugging. 3.8+ |
| Format a date | f"{today:%d %B %Y}" | 26 September 2026 |
| Literal braces | f"{{not a placeholder}}" | |
| Same quotes inside the braces | f"{user["name"]}" | 3.12+. Use the other quote type before that |
| Template string | t"Hello, {name}" | 3.14+. Builds a Template, not a str, so a library can escape the values |
| Length | len(s) | Characters, not bytes: len("café") is 4 |
| One character | s[0] s[-1] | First and last |
| Part of a string | s[1:4] s[:3] s[-3:] | The end position is not included |
| Reverse | s[::-1] | |
| Change case | s.upper() s.lower() s.title() | |
| Trim whitespace | s.strip() s.lstrip() s.rstrip() | |
| Remove a prefix or suffix | s.removeprefix("Mr ") s.removesuffix(".txt") | 3.9+. strip() removes characters, not a word |
| Split into a list | s.split(",") | s.split() with no argument splits on any run of whitespace |
| Split into lines | s.splitlines() | |
| Join a list into a string | ", ".join(words) | Every item must already be a string |
| Replace | s.replace("cat", "dog") | Every match. A third argument limits the count |
| Does it contain | "cat" in s | |
| Find the position | s.find("cat") | -1 when missing. s.index raises ValueError instead |
| Starts or ends with | s.startswith("http") s.endswith((".jpg", ".png")) | A tuple checks several at once |
| Count a substring | s.count("a") | |
| Only digits | s.isdigit() | False for "-1" and "1.5" |
| Repeat | "-" * 20 | |
| String across several lines | """...""" | Triple quotes. Keeps the newlines and the indentation |
| Raw string, for paths and regex | r"C:\new\folder" | Backslashes are kept as they are |
| Newline and tab | "line\n" "col\t" | |
| Character codes | ord("A") chr(65) | 65 and "A" |
| String to bytes and back | s.encode("utf-8") data.decode("utf-8") | |
| Older formatting you will still see | "{} has {} items".format(name, n) | And "%s has %d items" % (name, n) |
Lists
A list is an ordered, changeable sequence. It can hold anything, including other lists and a mix of types.
| Task | Code | Notes |
|---|---|---|
| Create a list | nums = [10, 20, 30] | Empty: [] |
| Read by position | nums[0] nums[-1] | IndexError past the end |
| Slice | nums[1:3] nums[:2] nums[::2] | A new list. The end is not included |
| How many items | len(nums) | |
| Add to the end | nums.append(40) | |
| Add several | nums.extend([50, 60]) | append([50, 60]) would add one item, a list |
| Insert at a position | nums.insert(0, 5) | |
| Remove the last item | nums.pop() | Returns the removed item |
| Remove at a position | nums.pop(0) del nums[0] | Slow on long lists. Use collections.deque for a queue |
| Remove by value | nums.remove(20) | First match only. ValueError if it is not there |
| Remove everything | nums.clear() | |
| Is it in the list | 20 in nums | |
| Find a position | nums.index(20) | ValueError if it is not there |
| Count a value | nums.count(20) | |
| Sort in place | nums.sort() | Returns None, so never nums = nums.sort() |
| Sorted copy | sorted(nums) | Works on any iterable. Always returns a list |
| Sort by a field | sorted(people, key=lambda p: p["age"]) | |
| Sort by two fields | sorted(people, key=lambda p: (p["team"], p["age"])) | Tuples compare item by item |
| Largest first | sorted(nums, reverse=True) | |
| Reverse | nums.reverse() nums[::-1] | In place, and a reversed copy |
| Copy | nums.copy() list(nums) nums[:] | Shallow: nested lists are still shared |
| Deep copy | import copy copy.deepcopy(grid) | |
| Join two lists | a + b | |
| First item and the rest | first, *rest = nums | |
| Sum, smallest, largest | sum(nums) min(nums) max(nums) | max(people, key=len) compares by a key |
| Are any or all true | any(x > 0 for x in nums) all(x > 0 for x in nums) | |
| Grid of zeros | grid = [[0] * 3 for _ in range(3)] | Not [[0] * 3] * 3, which repeats one inner list |
Dictionaries
A dict maps keys to values. Keys must be immutable, such as strings, numbers or tuples, and a dict remembers the order keys were added.
| Task | Code | Notes |
|---|---|---|
| Create a dict | user = {"name": "Ada", "age": 36} | Empty: {} |
| Create from keyword arguments | dict(name="Ada", age=36) | |
| Read a value | user["name"] | KeyError if the key is missing |
| Read with a fallback | user.get("email") user.get("email", "none") | None, or the fallback, when missing |
| Set a value | user["email"] = "ada@example.com" | |
| Remove a key | del user["age"] | KeyError if missing |
| Remove and return | user.pop("age", None) | With a fallback, no error when missing |
| Is the key there | "name" in user | Checks keys, not values |
| Loop over keys | for key in user: | |
| Loop over keys and values | for key, value in user.items(): | |
| Just the values | user.values() | |
| Keys as a list | list(user) | |
| How many keys | len(user) | |
| Merge into a new dict | merged = defaults | overrides | 3.9+. The right side wins. {**a, **b} before that |
| Merge into this dict | user |= {"age": 37} | 3.9+. Or user.update(...) |
| Set only if missing | user.setdefault("tags", []).append("admin") | Returns the existing value when there is one |
| Count how often each value appears | counts[w] = counts.get(w, 0) + 1 | Or collections.Counter |
| Build from two lists | dict(zip(keys, values)) | |
| Swap keys and values | {v: k for k, v in d.items()} | |
| Sort by value | sorted(d.items(), key=lambda kv: kv[1]) | A list of (key, value) pairs |
| Read a nested value safely | cfg.get("db", {}).get("host") | |
| Every key with the same value | dict.fromkeys(["a", "b"], 0) | Do not use a list as the value: all keys would share it |
Sets and tuples
A set holds unique items with no order and answers "is it in here" quickly. A tuple is a list that cannot change, used for fixed groups of values like coordinates.
| Task | Code | Notes |
|---|---|---|
| Create a set | tags = {"python", "sql"} | |
| Empty set | set() | {} is an empty dict |
| Remove duplicates | set(items) | Loses the order |
| Remove duplicates, keep the order | list(dict.fromkeys(items)) | |
| Add an item | tags.add("bash") | |
| Remove an item | tags.discard("sql") | No error when missing. remove raises KeyError |
| Is it in the set | "sql" in tags | Fast even for huge sets, unlike a list |
| In either, both, the first only | a | b a & b a - b | Union, intersection, difference |
| In one but not both | a ^ b | |
| Is every item also in the other | a <= b | Subset |
| Set that cannot change | frozenset(tags) | Can be a dict key or go in another set |
| Create a tuple | point = (3, 4) | |
| Tuple with one item | (1,) | The comma makes the tuple, not the brackets |
| Unpack | x, y = point | |
| Tuple as a dict key | walls[(0, 1)] = True | |
| Tuple with named fields | class Point(NamedTuple): x: int; y: int | from typing import NamedTuple. p.x and p[0] both work |
Comprehensions
A comprehension builds a list, dict or set from a loop in one expression. Read it as the for loop it replaces, with the value to keep written first.
| Task | Code | Notes |
|---|---|---|
| Transform every item | [x * 2 for x in nums] | |
| Keep some items | [x for x in nums if x > 0] | |
| Transform some, keep all | ["even" if x % 2 == 0 else "odd" for x in nums] | if/else before the for, a filter if after it |
| Build a dict | {name: len(name) for name in names} | |
| Build a set | {w.lower() for w in words} | |
| Feed a function without building a list | sum(x * x for x in nums) | A generator expression. Items are made one at a time |
| Flatten a list of lists | [x for row in grid for x in row] | The for clauses go in the same order as nested loops |
| Every pair | [(a, b) for a in xs for b in ys] | |
| With the position | [f"{i}. {item}" for i, item in enumerate(items, 1)] | |
| Filter on a computed value | [y for x in data if (y := parse(x)) is not None] | 3.8+. Computes parse(x) once |
| Filter a dict | {k: v for k, v in prices.items() if v < 10} |
If a comprehension needs more than one if or a second line to read, write the for loop instead. It runs at the same speed and is easier to change.
Control flow
Blocks are marked by a colon and indentation, not braces. Four spaces per level is the convention.
| Task | Code | Notes |
|---|---|---|
| If, elif, else | if x > 0: elif x < 0: else: | Each followed by its own indented block |
| And, or, not | a and b a or b not a | Words, not && || ! |
| Loop over a list | for item in items: | |
| Loop a number of times | for i in range(5): | 0 to 4. The end is not included |
| Count between two numbers | for i in range(1, 11): | 1 to 10 |
| Count down | for i in range(10, 0, -1): | |
| Loop with the position | for i, item in enumerate(items): | enumerate(items, 1) starts counting at 1 |
| Loop over two lists together | for name, score in zip(names, scores): | Stops at the shorter one. strict=True raises instead (3.10+) |
| Loop backwards | for item in reversed(items): | |
| Loop in sorted order | for key in sorted(d): | |
| While loop | while n > 0: | |
| Leave a loop | break | |
| Skip to the next item | continue | |
| Run code when a loop did not break | else: | Lined up with the for or while. Handy for searches |
| Do nothing, as a placeholder | pass | |
| Match a value | match command: case "start": case _: | 3.10+. Each case indented under match. case _ matches anything |
| Match several values | case "quit" | "exit": | |
| Match and unpack a list | case [first, *rest]: | |
| Match the shape of a dict | case {"type": "click", "x": x, "y": y}: | Extra keys are ignored |
| Match an object | case Point(x=0, y=y): | |
| Add a condition to a case | case int(n) if n < 0: | A guard |
Functions, *args and **kwargs
| Task | Code | Notes |
|---|---|---|
| Define a function | def add(a, b): return a + b | Without a return, it returns None |
| Default value | def greet(name="world"): | |
| Call with names | greet(name="Ada") | Keyword arguments, in any order |
| Return several values | return low, high | A tuple. Unpack with low, high = bounds(nums) |
| Any number of positional arguments | def total(*nums): return sum(nums) | nums is a tuple |
| Any number of keyword arguments | def tag(name, **attrs): | attrs is a dict |
| Spread a list into arguments | add(*pair) | |
| Spread a dict into keyword arguments | connect(**settings) | |
| Force keyword arguments | def connect(host, *, timeout=10): | Everything after * must be named |
| Force positional arguments | def clamp(x, lo, hi, /): | Everything before / cannot be named. 3.8+ |
| Every kind, in order | def f(a, /, b, *args, c, **kwargs): | |
| Pass everything straight on | return func(*args, **kwargs) | The usual body of a wrapper |
| Anonymous function | lambda x: x * 2 | One expression only |
| Type hints | def add(a: int, b: int) -> int: | |
| Might return nothing | def find(user_id: int) -> User | None: | 3.10+. Optional[User] before that |
| Generic function | def first[T](items: list[T]) -> T: | 3.12+ |
| Docstring | def area(r): """Area of a circle of radius r.""" | First line of the body. help() shows it |
| Generator | yield value | Inside a def, makes it a generator: values come out one at a time, lazily |
| Change a variable from the outer function | nonlocal count | |
| Change a module-level variable | global counter | Usually a sign to pass it in instead |
| Decorate a function | @functools.cache | On the line above def. Remembers results for the same arguments |
| Fix some arguments in advance | parse_binary = functools.partial(int, base=2) |
Classes and dataclasses
| Task | Code | Notes |
|---|---|---|
| Define a class | class Point: | |
| Set up each new object | def __init__(self, x, y): self.x, self.y = x, y | Indented inside the class |
| Create an object | p = Point(1, 2) | No new keyword |
| Define a method | def dist(self): return (self.x ** 2 + self.y ** 2) ** 0.5 | self is always the first parameter |
| What print and the shell show | def __repr__(self): return f"Point({self.x}, {self.y})" | __str__ for a friendlier print |
| Compare by value | def __eq__(self, other): | Without it, == only matches the same object |
| Shared by every instance | dimensions = 2 | In the class body, outside any method. A class attribute |
| Alternative constructor | @classmethod def from_tuple(cls, t): return cls(*t) | Called on the class: Point.from_tuple((1, 2)) |
| Function that needs no self | @staticmethod | |
| Computed attribute | @property def area(self): return self.w * self.h | Read as rect.area, no brackets |
| Inherit from a class | class Cat(Animal): | |
| Call the parent's method | super().__init__(name) | |
| Private by convention | self._cache = {} | One underscore says internal. Nothing enforces it |
| Dataclass | @dataclass class Point: | from dataclasses import dataclass. Fields go below as x: int. Writes __init__, __repr__ and __eq__ for you |
| List or dict as a default | tags: list[str] = field(default_factory=list) | A plain [] default is an error in a dataclass |
| Cannot be changed after creation | @dataclass(frozen=True) | Also makes it usable as a dict key |
| Sortable by its fields | @dataclass(order=True) | Compares fields top to bottom |
| Less memory per object | @dataclass(slots=True) | 3.10+ |
| Every field named when creating | @dataclass(kw_only=True) | 3.10+ |
| Check values after __init__ | def __post_init__(self): | |
| Copy with a change | dataclasses.replace(p, x=5) | copy.replace(p, x=5) in 3.13+ |
| To a dict | dataclasses.asdict(p) | Nested dataclasses too. Ready for json.dumps |
| Fixed set of named values | class Colour(Enum): RED = 1; GREEN = 2 | from enum import Enum. Colour.RED.name is "RED" |
| Method a subclass must write | @abstractmethod | from abc import ABC, abstractmethod. Subclass ABC |
Exceptions
| Task | Code | Notes |
|---|---|---|
| Catch an error | try: except ValueError: | Each followed by its own indented block |
| Get the error object | except ValueError as e: print(e) | |
| Catch several types | except (KeyError, IndexError): | 3.14 also allows it without brackets when there is no as |
| Run only if nothing went wrong | else: | After the except blocks |
| Run however the block ends | finally: | Runs after a return or an uncaught error too |
| Raise an error | raise ValueError("age must be positive") | |
| Raise it again after logging | raise | Inside an except block. Keeps the original traceback |
| Raise a new error with the cause | raise ConfigError("bad config") from e | The traceback shows both |
| Your own error type | class ConfigError(Exception): pass | |
| Check an assumption | assert total >= 0, "total went negative" | Skipped with python3 -O. Not for checking user input |
| Ignore one kind of error | with contextlib.suppress(FileNotFoundError): os.remove(path) | |
| Add context to an error | e.add_note(f"while reading {path}") | 3.11+. Shown under the traceback |
| Several errors at once | except* ValueError as group: | 3.11+. For ExceptionGroup, raised by asyncio.TaskGroup |
| Print the traceback and carry on | traceback.print_exc() |
Catch the narrowest error you can. A bare except: or except Exception: around a whole block also hides the typo in a variable name you would have wanted to hear about. Python style is to try the operation and handle the failure, rather than check every condition first.
Files
Open files with with, so they are closed even if something fails. Always pass encoding="utf-8" for text: the default depends on the operating system, and Windows is not UTF-8 everywhere.
| Task | Code | Notes |
|---|---|---|
| Read a whole file | with open("notes.txt", encoding="utf-8") as f: text = f.read() | |
| Read line by line | for line in f: | Each line keeps its \n. line.rstrip("\n") drops it |
| Read lines into a list | lines = f.read().splitlines() | |
| Write a file | with open("out.txt", "w", encoding="utf-8") as f: f.write("hi\n") | Replaces the file. write adds no newline |
| Add to the end of a file | open("log.txt", "a", encoding="utf-8") | |
| Create only if it does not exist | open("out.txt", "x", encoding="utf-8") | FileExistsError otherwise |
| Read bytes, for images and zips | open("photo.jpg", "rb") | |
| Print into a file | print("done", file=f) | |
| Build a path | path = Path("data") / "notes.txt" | from pathlib import Path. Works on every OS |
| Read or write text in one line | path.read_text(encoding="utf-8") path.write_text(text, encoding="utf-8") | |
| Does it exist | path.exists() path.is_file() path.is_dir() | |
| Make a folder | path.mkdir(parents=True, exist_ok=True) | No error if it is already there |
| List matching files | Path(".").glob("*.csv") | rglob("*.csv") searches subfolders too |
| Parts of a path | path.name path.stem path.suffix path.parent | notes.txt, notes, .txt, data |
| Rename or move | path.rename("archive/notes.txt") | |
| Copy a file | path.copy("backup/notes.txt") | 3.14+. shutil.copy2(src, dst) before that |
| Delete a file | path.unlink(missing_ok=True) | A folder and everything in it: shutil.rmtree(folder) |
| Home folder and current folder | Path.home() Path.cwd() | |
| Read JSON | data = json.loads(path.read_text(encoding="utf-8")) | json.load(f) with an open file |
| Write JSON | path.write_text(json.dumps(data, indent=2), encoding="utf-8") | |
| Read a CSV as dicts | for row in csv.DictReader(f): | Open it with newline="". Every value is a string |
| Write a CSV | writer = csv.writer(f) writer.writerow(["name", "score"]) | |
| Read a TOML config | with open("pyproject.toml", "rb") as f: cfg = tomllib.load(f) | 3.11+. Binary mode |
Common standard library modules
All of these ship with Python, so there is nothing to install. Import the module first: import collections, or from collections import Counter.
| Task | Code | Module |
|---|---|---|
| Count things | Counter(words).most_common(3) | collections |
| Dict that fills in missing keys | groups = defaultdict(list) groups[team].append(name) | collections |
| Queue with fast adds at both ends | q = deque(maxlen=100) q.append(x) q.popleft() | collections |
| Today and now | date.today() datetime.now() | datetime |
| Now in UTC | datetime.now(timezone.utc) | datetime. Store and compare times in UTC |
| Parse a date | date.fromisoformat("2026-09-26") | datetime. strptime(s, "%d/%m/%Y") for other formats |
| Add or subtract time | today + timedelta(days=7) | datetime |
| Time zone by name | ZoneInfo("Europe/London") | zoneinfo. pip install tzdata on Windows |
| Time how long something takes | start = time.perf_counter() | time |
| Wait | time.sleep(0.5) | time. Seconds |
| Random whole number from 1 to 6 | random.randint(1, 6) | random. Both ends included |
| Random item, shuffle, sample | random.choice(items) random.shuffle(items) random.sample(items, 3) | random |
| Password reset tokens and other secrets | secrets.token_urlsafe(32) | secrets. Never random for security |
| Unique ID | uuid.uuid4() uuid.uuid7() | uuid. uuid7 is 3.14+ and sorts by creation time |
| Find every match of a pattern | re.findall(r"\d+", s) | re |
| Pull out part of a match | m = re.search(r"(\d{4})-(\d{2})", s) m.group(1) | re. None when nothing matches |
| Replace by pattern | re.sub(r"\s+", " ", s) | re |
| Read an environment variable | os.environ.get("API_KEY") | os |
| Command-line arguments | sys.argv[1:] | sys. argparse for flags and --help |
| Exit with an error code | sys.exit(1) | sys |
| Run another program | subprocess.run(["git", "status"], capture_output=True, text=True, check=True) | subprocess. A list, not one string |
| Chunks of n | itertools.batched(items, 100) | itertools. 3.12+ |
| Neighbouring pairs | itertools.pairwise(nums) | itertools. 3.10+ |
| Every combination | itertools.combinations(items, 2) itertools.product(a, b) | itertools |
| Several iterables as one | itertools.chain(a, b) | itertools |
| Square root, pi, rounding | math.sqrt(2) math.pi math.floor(x) math.ceil(x) | math |
| Are two floats close enough | math.isclose(0.1 + 0.2, 0.3) | math |
| Average and median | statistics.mean(nums) statistics.median(nums) | statistics |
| Smallest or largest few | heapq.nlargest(3, scores) | heapq. Faster than sorting when you need only a few |
| Log messages with levels | logging.basicConfig(level=logging.INFO) | logging. Then logging.info("started") |
| Base64 encode and decode | base64.b64encode(b"hi") base64.b64decode("aGk=") | base64 |
| Print nested data readably | pprint(config) | pprint |
Virtual environments and pip
A virtual environment is a folder with its own Python and its own installed packages, so each project keeps its own versions. Make one per project and install packages into it, never into the system Python.
| Task | Command | Notes |
|---|---|---|
| Create a virtual environment | python3 -m venv .venv | In the project folder. .venv is the usual name |
| Activate it on macOS or Linux | source .venv/bin/activate | The prompt shows (.venv) |
| Activate it in Windows PowerShell | .venv\Scripts\Activate.ps1 | .venv\Scripts\activate.bat in Command Prompt |
| Check which Python is running | python -c "import sys; print(sys.prefix)" | Should end in .venv |
| Leave it | deactivate | |
| Run without activating | .venv/bin/python app.py | Handy in cron jobs and scripts |
| Install a package | python -m pip install requests | python -m pip installs for the python you are running |
| Install a range of versions | python -m pip install "requests>=2.32,<3" | Quote it, or the shell reads < and > as redirects |
| Upgrade a package | python -m pip install --upgrade requests | |
| Save what is installed | python -m pip freeze > requirements.txt | Exact versions of everything, dependencies included |
| Install from a requirements file | python -m pip install -r requirements.txt | |
| List installed packages | python -m pip list | --outdated shows what has a newer version |
| Show a package's version and dependencies | python -m pip show requests | |
| Remove a package | python -m pip uninstall requests | |
| Install your own project while you work on it | python -m pip install -e . | Needs a pyproject.toml or setup.py. Code changes apply without reinstalling |
| Upgrade pip itself | python -m pip install --upgrade pip | |
| Start again from scratch | rm -rf .venv | It is only a folder. Recreate it and install from requirements.txt |
Add .venv to .gitignore and commit requirements.txt instead. On recent Ubuntu, Debian, Fedora and Homebrew, pip install outside a virtual environment stops with an externally-managed-environment error: that is the operating system protecting its own Python, and a virtual environment is the fix.
A dataclass, start to finish
Most of the classes table in one place: typed fields, a list default, a computed
property, validation in __post_init__, sorting with order=True, and turning
the result into JSON.
import json
from dataclasses import asdict, dataclass, field
@dataclass(order=True)
class Book:
title: str
author: str = field(compare=False)
pages: int = field(default=0, compare=False)
tags: list[str] = field(default_factory=list, compare=False)
def __post_init__(self):
if self.pages < 0:
raise ValueError(f"pages must be 0 or more, got {self.pages}")
@property
def is_long(self) -> bool:
return self.pages > 500
shelf = [
Book("Dune", "Frank Herbert", 688, ["sci-fi"]),
Book("Beloved", "Toni Morrison", 324),
Book("Circe", "Madeline Miller", 393, ["myth"]),
]
print(shelf[1])
# Book(title='Beloved', author='Toni Morrison', pages=324, tags=[])
print([b.title for b in sorted(shelf)]) # ['Beloved', 'Circe', 'Dune']
print([b.title for b in shelf if b.is_long]) # ['Dune']
print(Book("Dune", "Someone else") == shelf[0]) # True: only title is compared
print(json.dumps(asdict(shelf[2])))
# {"title": "Circe", "author": "Madeline Miller", "pages": 393, "tags": ["myth"]}
try:
Book("Oops", "Nobody", -1)
except ValueError as e:
print(e) # pages must be 0 or more, got -1field(compare=False) leaves a field out of == and the sort order, which is why
two books with the same title compare equal. field(default_factory=list) gives
every book its own empty list; a plain tags: list[str] = [] is refused by the
decorator for exactly the reason in the gotchas below.
*args and **kwargs, start to finish
The stars collect extra arguments in a definition and spread them out in a call. A decorator uses both to wrap any function without knowing its signature.
import functools
import time
def describe(first, *args, sep=", ", **kwargs):
print(f"first={first!r} args={args} sep={sep!r} kwargs={kwargs}")
describe(1, 2, 3, colour="red")
# first=1 args=(2, 3) sep=', ' kwargs={'colour': 'red'}
numbers = [4, 5, 6]
options = {"sep": " | ", "debug": True}
describe(*numbers, **options)
# first=4 args=(5, 6) sep=' | ' kwargs={'debug': True}
def timed(func):
@functools.wraps(func) # keeps the wrapped function's name and docstring
def wrapper(*args, **kwargs):
start = time.perf_counter()
try:
return func(*args, **kwargs)
finally:
elapsed = time.perf_counter() - start
print(f"{func.__name__} took {elapsed:.3f}s")
return wrapper
@timed
def slow_add(a, b, *, delay=0.1):
time.sleep(delay)
return a + b
print(slow_add(2, 3, delay=0.2))
# slow_add took 0.200s
# 5sep sits after *args, so it can only be passed by name: describe(1, 2, " | ")
would put " | " into args. Without functools.wraps, every decorated function
would report its name as wrapper.
Files, start to finish
Read a CSV, total it up, and write the result as JSON, with pathlib for the
paths. This runs as it is: it writes its own sample file first.
import csv
import json
from collections import defaultdict
from pathlib import Path
data = Path("data")
data.mkdir(exist_ok=True)
sales_csv = data / "sales.csv"
sales_csv.write_text(
"region,product,amount\n"
"north,tea,12.50\n"
"south,tea,8.00\n"
"north,coffee,20.25\n",
encoding="utf-8",
)
totals = defaultdict(float)
with sales_csv.open(newline="", encoding="utf-8") as f:
for row in csv.DictReader(f):
totals[row["region"]] += float(row["amount"]) # CSV values are strings
report = data / "totals.json"
report.write_text(json.dumps(totals, indent=2), encoding="utf-8")
print(dict(totals)) # {'north': 32.75, 'south': 8.0}
print(report.name, report.stat().st_size, "bytes") # totals.json 36 bytes
for path in sorted(data.glob("*")):
print(path.suffix, path.stem)
# .csv sales
# .json totalsjson.dumps accepts the defaultdict because it is a dict underneath. To check
or reformat a JSON file by hand, paste it into the
JSON formatter, or run python3 -m json data/totals.json.
When the data outgrows a file, the built-in sqlite3 module gives you a real SQL
database in a single file with nothing to install. The
SQL cheat sheet covers the queries, and ends with a
sqlite3 example that passes values safely as parameters.
If the job is mostly tables, statistics and charts, the R cheat sheet does this same CSV-to-totals job with dplyr, and has a table of the differences for moving between the two languages.
Pattern matching with match
match compares a value against shapes rather than just values, and pulls out the
parts it needs. It suits parsed JSON and commands, where an if chain would check
the same keys over and over. It needs Python 3.10 or later.
from dataclasses import dataclass
@dataclass
class Point:
x: int
y: int
def handle(event):
match event:
case {"type": "click", "pos": Point(x=0, y=0)}:
return "click at the origin"
case {"type": "click", "pos": Point(x=x, y=y)}:
return f"click at {x}, {y}"
case {"type": "key", "key": "q" | "escape"}:
return "quit"
case {"type": "key", "key": str(key)} if len(key) == 1:
return f"typed {key}"
case [first, *rest]:
return f"batch of {1 + len(rest)}, starting with {handle(first)}"
case _:
return "unknown event"
print(handle({"type": "click", "pos": Point(0, 0)})) # click at the origin
print(handle({"type": "click", "pos": Point(3, 4)})) # click at 3, 4
print(handle({"type": "key", "key": "escape"})) # quit
print(handle({"type": "key", "key": "a", "shift": True})) # typed a
print(handle([{"type": "key", "key": "q"}, {}])) # batch of 2, starting with quit
print(handle("hello")) # unknown eventCases are tried top to bottom and the first match wins, so put specific cases above
general ones. A bare name such as x in a pattern captures a value, it does not
compare against a variable called x. A string never matches a sequence pattern
like [first, *rest], which is why "hello" falls through to case _.
A project with a virtual environment
The usual first five minutes of a new project on macOS or Linux. On Windows, swap
the source line for .venv\Scripts\Activate.ps1.
mkdir weather && cd weather
python3 -m venv .venv
source .venv/bin/activate
python -m pip install requests
python -m pip freeze > requirements.txt
echo ".venv/" >> .gitignore
git init && git add . && git commit -m "Start weather project"
deactivateactivate has to be run with source because it changes your current shell's
PATH and prompt, and a script run the normal way gets a shell of its own. The
Bash cheat sheet covers
the difference, and the rest of scripting your terminal.
Someone cloning the project later recreates the environment from
requirements.txt:
python3 -m venv .venv
source .venv/bin/activate
python -m pip install -r requirements.txtThe Git cheat sheet covers the rest of that workflow,
and the Docker cheat sheet covers shipping the
same project in a container, where the official python:3.14 image replaces the
virtual environment.
Gotchas
The mistakes almost everyone makes in their first month of Python.
| Looks right | What actually happens | Do this instead |
|---|---|---|
def add(item, items=[]): | The same list is reused on every call, so it keeps growing | items=None, then if items is None: items = [] |
grid = [[0] * 3] * 3 | Three references to one row: change one, change all | [[0] * 3 for _ in range(3)] |
copy = nums | Both names point at the same list | nums.copy(), or copy.deepcopy for nested data |
nums = nums.sort() | nums is now None: sort works in place | nums.sort(), or nums = sorted(nums) |
if x == None: | Works, but can be fooled by a custom __eq__ | if x is None: |
empty = {} for a set | That is an empty dict | set() |
(1) for a one-item tuple | Just the number 1 | (1,) |
for item in items: items.remove(item) | Skips every other item | items = [i for i in items if keep(i)] |
"Total: " + 5 | TypeError: no automatic conversion | f"Total: {5}" |
0.1 + 0.2 == 0.3 | False: the sum is 0.30000000000000004 | math.isclose(0.1 + 0.2, 0.3) |
round(2.5) | 2: halves round to the even number | Decimal("2.5").quantize(Decimal("1"), ROUND_HALF_UP) |
except: around a whole function | Also catches typos, Ctrl+C and sys.exit | Catch the specific error you expect |
A file called random.py next to your script | import random imports your file, not the standard library | Rename your file |
open("data.txt") with no encoding | Uses the OS default, which is not UTF-8 everywhere on Windows | open("data.txt", encoding="utf-8") |
pip install requests works, import requests fails | pip belonged to a different Python | python -m pip install requests |
sorted and list.sort use Timsort, a mix of
merge sort and
insertion sort. It is stable,
so items that compare equal keep their original order, which is what makes sorting
by one key and then another work. Both are on the site as step-through
visualisations.
Common questions
Which version of Python does this cheat sheet cover?
Python 3.14, the current version, checked against the 3.14.7 release. Almost everything also works in 3.10 and later. Anything newer than 3.8 says so in the notes column: match needs 3.10, the type statement and generic functions need 3.12, and template strings, python3 -m json and Path.copy need 3.14. Run python3 --version to see which version you have.
What is the difference between a list and a tuple in Python?
A list can change after you create it: you can append, remove and sort it in place. A tuple cannot. Use a list for a collection of similar things that grows or shrinks, like rows from a file, and a tuple for a fixed group of values that belong together, like an (x, y) point or a (key, value) pair. Because a tuple cannot change, it can be a dictionary key or go in a set, and a list cannot.
What do *args and **kwargs mean?
In a function definition, *args collects any extra positional arguments into a tuple and **kwargs collects any extra keyword arguments into a dict. The names are only a convention; the stars do the work. In a function call the stars work the other way round: *items spreads a list into separate arguments and **options spreads a dict into keyword arguments. Wrappers and decorators use both, def wrapper(*args, **kwargs) followed by func(*args, **kwargs), to pass along whatever they were given.
When should I use a dataclass instead of a normal class?
Use a dataclass when the class is mostly data: a few named fields with types. The decorator writes __init__, __repr__ and __eq__ from the field list, and options add sorting, immutability and slots. Use a normal class when construction needs real logic, when the object mainly holds behaviour rather than data, or when the fields are not known up front. A dataclass is still an ordinary class, so it can have methods and properties too.
What is the difference between == and is?
== asks whether two values are equal. is asks whether they are the same object in memory. Two separate lists with the same contents are == but not is. Use is only for None, True and False, which there is exactly one of, and == for everything else. Comparing numbers or strings with is can seem to work for small values and then fail for larger ones.
Do I need a virtual environment?
For any project that installs packages, yes. Without one, every project on the machine shares one set of package versions, so upgrading a library for one project can break another, and many Linux distributions and Homebrew now refuse pip install outside a virtual environment. Create one with python3 -m venv .venv in the project folder, activate it, and install into it. A script that only uses the standard library can run without one.
What is the difference between pip and uv?
pip is the package installer that comes with Python. uv is a separate, much faster tool that can create virtual environments, install packages, pin versions in a lock file and even install Python itself. uv venv and uv pip install mirror the commands on this page, so moving between them is easy. pip is always there; uv has to be installed first.
Why is 0.1 + 0.2 not equal to 0.3 in Python?
Floats are stored in binary, and 0.1 and 0.2 have no exact binary form, so their sum is 0.30000000000000004. Every language that uses standard floating point does the same. Round when you display a value, compare floats with math.isclose rather than ==, and use the decimal module when exact decimal arithmetic matters, as it does for money.
