Scripting and data cheat sheetPython logo™

Python cheat sheet

Python syntax on one page, grouped by what you are trying to do: f-strings, dicts, comprehensions, dataclasses, files and venvs, checked against Python 3.14.

Last updated

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.

Running Python

TaskCommandNotes
Check which version you havepython3 --versionOn Windows, py --version
Run a scriptpython3 app.py
Open an interactive shellpython3Leave with exit() or Ctrl+D (Ctrl+Z then Enter on Windows)
Run a script, then stay in the shellpython3 -i app.pyEvery variable from the script is still there
Run one line without a filepython3 -c 'print(2 ** 100)'
Run a module as a programpython3 -m http.server 8000Serves the current folder on localhost:8000
Pretty-print a JSON filepython3 -m json data.json3.14+. python3 -m json.tool on older versions
Time a small piece of codepython3 -m timeit "sum(range(1000))"
Check a file for syntax errorspython3 -m py_compile app.py
Run the tests in a folderpython3 -m unittest discover
Only run code when the file is run directlyif __name__ == "__main__":Not when another file imports it
Make a script executable on Linux or macOS#!/usr/bin/env python3First line of the file, then chmod +x app.py
Read the docs for anythinghelp(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

TaskCodeNotes
Print a lineprint("Hello")Several arguments are separated by a space
Print without a newlineprint("Loading", end="")
Assign a variablename = "Ada"No keyword and no declared type. snake_case by convention
Constant, by conventionMAX_USERS = 100Capitals say do not change it. Nothing stops you
Assign several at oncea, b = 1, 2
Swap two variablesa, b = b, a
Comment# one lineNo block comment. Use # on each line
What type is ittype(value)type(3) is <class 'int'>
Is it a kind ofisinstance(value, int)True for subclasses too, so isinstance(True, int) is True
Is it one of several typesisinstance(value, int | float)3.10+. A tuple (int, float) works everywhere
No valueresult = None
Is it Nonevalue is NoneUse is, not ==
Numbers42 3.14 1_000_000 2e3Underscores are ignored. Integers never overflow
Divide7 / 2Always a float: 3.5
Floor division7 // 23. Rounds down, so -7 // 2 is -4
Remainder7 % 3Takes the sign of the divisor: -7 % 3 is 2
Both at oncedivmod(17, 5)(3, 2)
Raise to a power2 ** 101024. Not ^, which is bitwise xor
Roundround(3.14159, 2)Halves go to the even number: round(2.5) is 2
String to numberint("42") float("3.5")ValueError when it is not a number
Parse in another baseint("ff", 16)255
Number to stringstr(42)
True and falseTrue FalseCapitalised
Default when falsyname = user_input or "Anonymous"Also replaces 0 and empty strings
Pick one of two valueslabel = "item" if n == 1 else "items"
Chain comparisons0 <= x < 10
Assign inside an expressionif (n := len(items)) > 10:The walrus operator. 3.8+
Type hintcount: int = 0Not checked when the code runs. Tools like mypy read it
Name a typetype UserId = int3.12+. UserId: TypeAlias = int before that
Exact decimals, for moneyfrom 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.

TaskCodeNotes
Single or double quotes'cat' "cat"The same. Pick one and stick to it
Put a value in a stringf"Hello, {name}"An f-string. Any expression goes inside the braces
Two decimal placesf"{price:.2f}"3.14159 becomes 3.14
Thousands separatorf"{n:,}"1234567 becomes 1,234,567
Percentagef"{ratio:.1%}"0.256 becomes 25.6%
Pad and alignf"{name:<10}" f"{name:>10}" f"{name:^10}"Left, right, centre in 10 characters
Pad a number with zerosf"{7:03}"007
Show the name and the valuef"{total=}"total=42. For debugging. 3.8+
Format a datef"{today:%d %B %Y}"26 September 2026
Literal bracesf"{{not a placeholder}}"
Same quotes inside the bracesf"{user["name"]}"3.12+. Use the other quote type before that
Template stringt"Hello, {name}"3.14+. Builds a Template, not a str, so a library can escape the values
Lengthlen(s)Characters, not bytes: len("café") is 4
One characters[0] s[-1]First and last
Part of a strings[1:4] s[:3] s[-3:]The end position is not included
Reverses[::-1]
Change cases.upper() s.lower() s.title()
Trim whitespaces.strip() s.lstrip() s.rstrip()
Remove a prefix or suffixs.removeprefix("Mr ") s.removesuffix(".txt")3.9+. strip() removes characters, not a word
Split into a lists.split(",")s.split() with no argument splits on any run of whitespace
Split into liness.splitlines()
Join a list into a string", ".join(words)Every item must already be a string
Replaces.replace("cat", "dog")Every match. A third argument limits the count
Does it contain"cat" in s
Find the positions.find("cat")-1 when missing. s.index raises ValueError instead
Starts or ends withs.startswith("http") s.endswith((".jpg", ".png"))A tuple checks several at once
Count a substrings.count("a")
Only digitss.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 regexr"C:\new\folder"Backslashes are kept as they are
Newline and tab"line\n" "col\t"
Character codesord("A") chr(65)65 and "A"
String to bytes and backs.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.

TaskCodeNotes
Create a listnums = [10, 20, 30]Empty: []
Read by positionnums[0] nums[-1]IndexError past the end
Slicenums[1:3] nums[:2] nums[::2]A new list. The end is not included
How many itemslen(nums)
Add to the endnums.append(40)
Add severalnums.extend([50, 60])append([50, 60]) would add one item, a list
Insert at a positionnums.insert(0, 5)
Remove the last itemnums.pop()Returns the removed item
Remove at a positionnums.pop(0) del nums[0]Slow on long lists. Use collections.deque for a queue
Remove by valuenums.remove(20)First match only. ValueError if it is not there
Remove everythingnums.clear()
Is it in the list20 in nums
Find a positionnums.index(20)ValueError if it is not there
Count a valuenums.count(20)
Sort in placenums.sort()Returns None, so never nums = nums.sort()
Sorted copysorted(nums)Works on any iterable. Always returns a list
Sort by a fieldsorted(people, key=lambda p: p["age"])
Sort by two fieldssorted(people, key=lambda p: (p["team"], p["age"]))Tuples compare item by item
Largest firstsorted(nums, reverse=True)
Reversenums.reverse() nums[::-1]In place, and a reversed copy
Copynums.copy() list(nums) nums[:]Shallow: nested lists are still shared
Deep copyimport copy copy.deepcopy(grid)
Join two listsa + b
First item and the restfirst, *rest = nums
Sum, smallest, largestsum(nums) min(nums) max(nums)max(people, key=len) compares by a key
Are any or all trueany(x > 0 for x in nums) all(x > 0 for x in nums)
Grid of zerosgrid = [[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.

TaskCodeNotes
Create a dictuser = {"name": "Ada", "age": 36}Empty: {}
Create from keyword argumentsdict(name="Ada", age=36)
Read a valueuser["name"]KeyError if the key is missing
Read with a fallbackuser.get("email") user.get("email", "none")None, or the fallback, when missing
Set a valueuser["email"] = "ada@example.com"
Remove a keydel user["age"]KeyError if missing
Remove and returnuser.pop("age", None)With a fallback, no error when missing
Is the key there"name" in userChecks keys, not values
Loop over keysfor key in user:
Loop over keys and valuesfor key, value in user.items():
Just the valuesuser.values()
Keys as a listlist(user)
How many keyslen(user)
Merge into a new dictmerged = defaults | overrides3.9+. The right side wins. {**a, **b} before that
Merge into this dictuser |= {"age": 37}3.9+. Or user.update(...)
Set only if missinguser.setdefault("tags", []).append("admin")Returns the existing value when there is one
Count how often each value appearscounts[w] = counts.get(w, 0) + 1Or collections.Counter
Build from two listsdict(zip(keys, values))
Swap keys and values{v: k for k, v in d.items()}
Sort by valuesorted(d.items(), key=lambda kv: kv[1])A list of (key, value) pairs
Read a nested value safelycfg.get("db", {}).get("host")
Every key with the same valuedict.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.

TaskCodeNotes
Create a settags = {"python", "sql"}
Empty setset(){} is an empty dict
Remove duplicatesset(items)Loses the order
Remove duplicates, keep the orderlist(dict.fromkeys(items))
Add an itemtags.add("bash")
Remove an itemtags.discard("sql")No error when missing. remove raises KeyError
Is it in the set"sql" in tagsFast even for huge sets, unlike a list
In either, both, the first onlya | b a & b a - bUnion, intersection, difference
In one but not botha ^ b
Is every item also in the othera <= bSubset
Set that cannot changefrozenset(tags)Can be a dict key or go in another set
Create a tuplepoint = (3, 4)
Tuple with one item(1,)The comma makes the tuple, not the brackets
Unpackx, y = point
Tuple as a dict keywalls[(0, 1)] = True
Tuple with named fieldsclass Point(NamedTuple): x: int; y: intfrom 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.

TaskCodeNotes
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 listsum(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.

TaskCodeNotes
If, elif, elseif x > 0: elif x < 0: else:Each followed by its own indented block
And, or, nota and b a or b not aWords, not && || !
Loop over a listfor item in items:
Loop a number of timesfor i in range(5):0 to 4. The end is not included
Count between two numbersfor i in range(1, 11):1 to 10
Count downfor i in range(10, 0, -1):
Loop with the positionfor i, item in enumerate(items):enumerate(items, 1) starts counting at 1
Loop over two lists togetherfor name, score in zip(names, scores):Stops at the shorter one. strict=True raises instead (3.10+)
Loop backwardsfor item in reversed(items):
Loop in sorted orderfor key in sorted(d):
While loopwhile n > 0:
Leave a loopbreak
Skip to the next itemcontinue
Run code when a loop did not breakelse:Lined up with the for or while. Handy for searches
Do nothing, as a placeholderpass
Match a valuematch command: case "start": case _:3.10+. Each case indented under match. case _ matches anything
Match several valuescase "quit" | "exit":
Match and unpack a listcase [first, *rest]:
Match the shape of a dictcase {"type": "click", "x": x, "y": y}:Extra keys are ignored
Match an objectcase Point(x=0, y=y):
Add a condition to a casecase int(n) if n < 0:A guard

Functions, *args and **kwargs

TaskCodeNotes
Define a functiondef add(a, b): return a + bWithout a return, it returns None
Default valuedef greet(name="world"):
Call with namesgreet(name="Ada")Keyword arguments, in any order
Return several valuesreturn low, highA tuple. Unpack with low, high = bounds(nums)
Any number of positional argumentsdef total(*nums): return sum(nums)nums is a tuple
Any number of keyword argumentsdef tag(name, **attrs):attrs is a dict
Spread a list into argumentsadd(*pair)
Spread a dict into keyword argumentsconnect(**settings)
Force keyword argumentsdef connect(host, *, timeout=10):Everything after * must be named
Force positional argumentsdef clamp(x, lo, hi, /):Everything before / cannot be named. 3.8+
Every kind, in orderdef f(a, /, b, *args, c, **kwargs):
Pass everything straight onreturn func(*args, **kwargs)The usual body of a wrapper
Anonymous functionlambda x: x * 2One expression only
Type hintsdef add(a: int, b: int) -> int:
Might return nothingdef find(user_id: int) -> User | None:3.10+. Optional[User] before that
Generic functiondef first[T](items: list[T]) -> T:3.12+
Docstringdef area(r): """Area of a circle of radius r."""First line of the body. help() shows it
Generatoryield valueInside a def, makes it a generator: values come out one at a time, lazily
Change a variable from the outer functionnonlocal count
Change a module-level variableglobal counterUsually a sign to pass it in instead
Decorate a function@functools.cacheOn the line above def. Remembers results for the same arguments
Fix some arguments in advanceparse_binary = functools.partial(int, base=2)

Classes and dataclasses

TaskCodeNotes
Define a classclass Point:
Set up each new objectdef __init__(self, x, y): self.x, self.y = x, yIndented inside the class
Create an objectp = Point(1, 2)No new keyword
Define a methoddef dist(self): return (self.x ** 2 + self.y ** 2) ** 0.5self is always the first parameter
What print and the shell showdef __repr__(self): return f"Point({self.x}, {self.y})"__str__ for a friendlier print
Compare by valuedef __eq__(self, other):Without it, == only matches the same object
Shared by every instancedimensions = 2In 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.hRead as rect.area, no brackets
Inherit from a classclass Cat(Animal):
Call the parent's methodsuper().__init__(name)
Private by conventionself._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 defaulttags: 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 changedataclasses.replace(p, x=5)copy.replace(p, x=5) in 3.13+
To a dictdataclasses.asdict(p)Nested dataclasses too. Ready for json.dumps
Fixed set of named valuesclass Colour(Enum): RED = 1; GREEN = 2from enum import Enum. Colour.RED.name is "RED"
Method a subclass must write@abstractmethodfrom abc import ABC, abstractmethod. Subclass ABC

Exceptions

TaskCodeNotes
Catch an errortry: except ValueError:Each followed by its own indented block
Get the error objectexcept ValueError as e: print(e)
Catch several typesexcept (KeyError, IndexError):3.14 also allows it without brackets when there is no as
Run only if nothing went wrongelse:After the except blocks
Run however the block endsfinally:Runs after a return or an uncaught error too
Raise an errorraise ValueError("age must be positive")
Raise it again after loggingraiseInside an except block. Keeps the original traceback
Raise a new error with the causeraise ConfigError("bad config") from eThe traceback shows both
Your own error typeclass ConfigError(Exception): pass
Check an assumptionassert total >= 0, "total went negative"Skipped with python3 -O. Not for checking user input
Ignore one kind of errorwith contextlib.suppress(FileNotFoundError): os.remove(path)
Add context to an errore.add_note(f"while reading {path}")3.11+. Shown under the traceback
Several errors at onceexcept* ValueError as group:3.11+. For ExceptionGroup, raised by asyncio.TaskGroup
Print the traceback and carry ontraceback.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.

TaskCodeNotes
Read a whole filewith open("notes.txt", encoding="utf-8") as f: text = f.read()
Read line by linefor line in f:Each line keeps its \n. line.rstrip("\n") drops it
Read lines into a listlines = f.read().splitlines()
Write a filewith 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 fileopen("log.txt", "a", encoding="utf-8")
Create only if it does not existopen("out.txt", "x", encoding="utf-8")FileExistsError otherwise
Read bytes, for images and zipsopen("photo.jpg", "rb")
Print into a fileprint("done", file=f)
Build a pathpath = Path("data") / "notes.txt"from pathlib import Path. Works on every OS
Read or write text in one linepath.read_text(encoding="utf-8") path.write_text(text, encoding="utf-8")
Does it existpath.exists() path.is_file() path.is_dir()
Make a folderpath.mkdir(parents=True, exist_ok=True)No error if it is already there
List matching filesPath(".").glob("*.csv")rglob("*.csv") searches subfolders too
Parts of a pathpath.name path.stem path.suffix path.parentnotes.txt, notes, .txt, data
Rename or movepath.rename("archive/notes.txt")
Copy a filepath.copy("backup/notes.txt")3.14+. shutil.copy2(src, dst) before that
Delete a filepath.unlink(missing_ok=True)A folder and everything in it: shutil.rmtree(folder)
Home folder and current folderPath.home() Path.cwd()
Read JSONdata = json.loads(path.read_text(encoding="utf-8"))json.load(f) with an open file
Write JSONpath.write_text(json.dumps(data, indent=2), encoding="utf-8")
Read a CSV as dictsfor row in csv.DictReader(f):Open it with newline="". Every value is a string
Write a CSVwriter = csv.writer(f) writer.writerow(["name", "score"])
Read a TOML configwith 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.

TaskCodeModule
Count thingsCounter(words).most_common(3)collections
Dict that fills in missing keysgroups = defaultdict(list) groups[team].append(name)collections
Queue with fast adds at both endsq = deque(maxlen=100) q.append(x) q.popleft()collections
Today and nowdate.today() datetime.now()datetime
Now in UTCdatetime.now(timezone.utc)datetime. Store and compare times in UTC
Parse a datedate.fromisoformat("2026-09-26")datetime. strptime(s, "%d/%m/%Y") for other formats
Add or subtract timetoday + timedelta(days=7)datetime
Time zone by nameZoneInfo("Europe/London")zoneinfo. pip install tzdata on Windows
Time how long something takesstart = time.perf_counter()time
Waittime.sleep(0.5)time. Seconds
Random whole number from 1 to 6random.randint(1, 6)random. Both ends included
Random item, shuffle, samplerandom.choice(items) random.shuffle(items) random.sample(items, 3)random
Password reset tokens and other secretssecrets.token_urlsafe(32)secrets. Never random for security
Unique IDuuid.uuid4() uuid.uuid7()uuid. uuid7 is 3.14+ and sorts by creation time
Find every match of a patternre.findall(r"\d+", s)re
Pull out part of a matchm = re.search(r"(\d{4})-(\d{2})", s) m.group(1)re. None when nothing matches
Replace by patternre.sub(r"\s+", " ", s)re
Read an environment variableos.environ.get("API_KEY")os
Command-line argumentssys.argv[1:]sys. argparse for flags and --help
Exit with an error codesys.exit(1)sys
Run another programsubprocess.run(["git", "status"], capture_output=True, text=True, check=True)subprocess. A list, not one string
Chunks of nitertools.batched(items, 100)itertools. 3.12+
Neighbouring pairsitertools.pairwise(nums)itertools. 3.10+
Every combinationitertools.combinations(items, 2) itertools.product(a, b)itertools
Several iterables as oneitertools.chain(a, b)itertools
Square root, pi, roundingmath.sqrt(2) math.pi math.floor(x) math.ceil(x)math
Are two floats close enoughmath.isclose(0.1 + 0.2, 0.3)math
Average and medianstatistics.mean(nums) statistics.median(nums)statistics
Smallest or largest fewheapq.nlargest(3, scores)heapq. Faster than sorting when you need only a few
Log messages with levelslogging.basicConfig(level=logging.INFO)logging. Then logging.info("started")
Base64 encode and decodebase64.b64encode(b"hi") base64.b64decode("aGk=")base64
Print nested data readablypprint(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.

TaskCommandNotes
Create a virtual environmentpython3 -m venv .venvIn the project folder. .venv is the usual name
Activate it on macOS or Linuxsource .venv/bin/activateThe prompt shows (.venv)
Activate it in Windows PowerShell.venv\Scripts\Activate.ps1.venv\Scripts\activate.bat in Command Prompt
Check which Python is runningpython -c "import sys; print(sys.prefix)"Should end in .venv
Leave itdeactivate
Run without activating.venv/bin/python app.pyHandy in cron jobs and scripts
Install a packagepython -m pip install requestspython -m pip installs for the python you are running
Install a range of versionspython -m pip install "requests>=2.32,<3"Quote it, or the shell reads < and > as redirects
Upgrade a packagepython -m pip install --upgrade requests
Save what is installedpython -m pip freeze > requirements.txtExact versions of everything, dependencies included
Install from a requirements filepython -m pip install -r requirements.txt
List installed packagespython -m pip list--outdated shows what has a newer version
Show a package's version and dependenciespython -m pip show requests
Remove a packagepython -m pip uninstall requests
Install your own project while you work on itpython -m pip install -e .Needs a pyproject.toml or setup.py. Code changes apply without reinstalling
Upgrade pip itselfpython -m pip install --upgrade pip
Start again from scratchrm -rf .venvIt 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 -1

field(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
# 5

sep 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 totals

json.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 event

Cases 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"
 
deactivate

activate 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.txt

The 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 rightWhat actually happensDo this instead
def add(item, items=[]):The same list is reused on every call, so it keeps growingitems=None, then if items is None: items = []
grid = [[0] * 3] * 3Three references to one row: change one, change all[[0] * 3 for _ in range(3)]
copy = numsBoth names point at the same listnums.copy(), or copy.deepcopy for nested data
nums = nums.sort()nums is now None: sort works in placenums.sort(), or nums = sorted(nums)
if x == None:Works, but can be fooled by a custom __eq__if x is None:
empty = {} for a setThat is an empty dictset()
(1) for a one-item tupleJust the number 1(1,)
for item in items: items.remove(item)Skips every other itemitems = [i for i in items if keep(i)]
"Total: " + 5TypeError: no automatic conversionf"Total: {5}"
0.1 + 0.2 == 0.3False: the sum is 0.30000000000000004math.isclose(0.1 + 0.2, 0.3)
round(2.5)2: halves round to the even numberDecimal("2.5").quantize(Decimal("1"), ROUND_HALF_UP)
except: around a whole functionAlso catches typos, Ctrl+C and sys.exitCatch the specific error you expect
A file called random.py next to your scriptimport random imports your file, not the standard libraryRename your file
open("data.txt") with no encodingUses the OS default, which is not UTF-8 everywhere on Windowsopen("data.txt", encoding="utf-8")
pip install requests works, import requests failspip belonged to a different Pythonpython -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.

See all cheat sheets

Want this explained by a cat?

The videos cover the same ground in sixty seconds. If there is a tool you want a cheat sheet for next, ask.