Knowledge/Python/Introduction
20 Chapters
3.12 Baseline
Living Document
Free License
Engineering Publication · Syed Omar Ibn Feroj← Back to portfolio
Py
Engineering Notes · Open Knowledge Repository

Python — Engineering
Notes for Working Code

A working reference for Python as it's actually written — the data model, the idioms that separate fluent Python from translated Java, and the standard library you should reach for before installing anything.

20
Chapters
3.12
Baseline
Living
Document
Free
License
Ch. 01
Getting Started
Running Python, the REPL, and the one environment habit that prevents most setup pain.
1.1 — Run it three ways
Shell
$ python3 script.py        # run a file
$ python3                   # interactive REPL
$ python3 -m http.server   # run a module as a script
1.2 — Always use a virtual environment
Shell
$ python3 -m venv .venv
$ source .venv/bin/activate   # Windows: .venv\\Scripts\\activate
$ pip install requests
$ pip freeze > requirements.txt
Never pip install into the system Python. A per-project venv is the single habit that eliminates "works on my machine" dependency drift. Modern alternative: uv or pipx for tools.
Ch. 02
Variables & Types
Dynamic typing, the object model, and why everything is a reference.
2.1 — Names bind to objects
Python
x = 42            # x is a name bound to an int object
y = x             # y binds to the SAME object
type(x)           # <class 'int'>
id(x) == id(y)   # True — same object

# int, float, complex, bool, str, bytes,
# list, tuple, dict, set, frozenset, None
2.2 — Mutable vs immutable

Immutable: int, float, str, tuple, frozenset, bytes. Mutable: list, dict, set. This distinction drives the most common Python footgun:

Python
def add(x, items=[]):   # DANGER: shared default
    items.append(x)
    return items
add(1); add(2)      # [1, 2] — surprise!

def add(x, items=None):  # correct pattern
    items = items if items is not None else []
Default argument values are evaluated once, at function definition. A mutable default ([], {}) is shared across all calls. Always default to None and build inside.
Ch. 03
Strings
f-strings, the methods you'll use daily, and bytes vs str.
3.1 — f-strings do everything
Python
name, n = "omar", 3
f"hi {name.title()}, x{n:03d}"   # 'hi Omar, x003'
f"{n=}"                          # 'n=3'  (debug form)
f"{3.14159:.2f}"                  # '3.14'
3.2 — Workhorse methods
Python
"  hi  ".strip()            # 'hi'
"a,b,c".split(",")        # ['a','b','c']
"-".join(["a","b"])        # 'a-b'
"Hello".startswith("He")   # True
"abcabc".replace("a","X")   # 'XbcXbc'

# text is str; raw I/O is bytes — encode/decode at the edge
"é".encode("utf-8")          # b'\\xc3\\xa9'
Ch. 04
Numbers
Arbitrary-precision ints, float pitfalls, and Decimal.
4.1 — ints don't overflow; floats do lie
Python
2 ** 100              # exact — Python ints are unbounded
7 // 2, 7 % 2         # 3, 1  (floor div, mod)
0.1 + 0.2 == 0.3      # False — binary float

from decimal import Decimal
Decimal("0.1") + Decimal("0.2")  # Decimal('0.3') — use for money
Never compare floats with ==. Use math.isclose(a, b), and use decimal.Decimal or integer cents for currency.
Ch. 05
Control Flow
Truthiness, the for-else, and structural pattern matching.
5.1 — Truthiness and the walrus
Python
# Falsy: 0, 0.0, '', [], , set(), None, False
if not items: ...        # pythonic empty check

while (line := f.readline()):  # walrus: assign + test
    process(line)
5.2 — match (3.10+)
Python
match command.split():
    case ["go", direction]:
        move(direction)
    case ["quit"]:
        return
    case _:
        print("unknown")
Ch. 06
Lists & Tuples
The sequence types — and when a tuple says something a list can't.
6.1 — List operations and unpacking
Python
xs = [1, 2, 3]
xs.append(4); xs.pop(); xs.insert(0, 9)
a, *rest = xs            # a=9, rest=[1,2,3]
first, *_, last = xs     # ignore the middle

pt = (3, 4)              # tuple — fixed, hashable
x, y = pt                # unpack
Use a tuple when the position has meaning and the size is fixed (a coordinate, a DB row); use a list when it's a homogeneous, growable collection. A tuple can be a dict key; a list can't.
Ch. 07
Dicts & Sets
The hash-backed workhorses — insertion-ordered since 3.7.
7.1 — Dict patterns
Python
d = {"a": 1}
d.get("b", 0)              # 0 — no KeyError
d.setdefault("c", []).append(1)
{**d, "b": 2}             # merge (3.9+: d1 | d2)

from collections import Counter, defaultdict
Counter("abracadabra").most_common(2)
g = defaultdict(list); g["k"].append(1)
7.2 — Sets for membership and dedup
Python
seen = set(); seen.add(x)
list(dict.fromkeys(xs))   # dedup, order-preserving
a & b, a | b, a - b      # intersection, union, difference
Ch. 08
Comprehensions
The single most distinctively-Python construct.
8.1 — List / dict / set / generator
Python
[x*x for x in range(5) if x % 2]   # [1, 9]
{k: v for k, v in pairs}            # dict comp
{c for c in word}                   # set comp
(x*x for x in range(10**9))      # lazy generator — O(1) memory
A comprehension should fit on one line and read clearly. If it needs nested loops and conditions, write the explicit for — clever one-liners are a maintenance tax.
Ch. 09
Slicing
The [start:stop:step] notation that works on every sequence.
9.1 — Slice everything
Python
xs[1:4]      # items 1,2,3
xs[:3], xs[3:]   # head / tail
xs[::-1]      # reversed copy
xs[::2]       # every other
xs[:] = [1,2]   # replace contents in place
s[1:4]       # works on strings/tuples too
Ch. 10
Functions
First-class functions, closures, lambdas.
10.1 — Closures and first-class use
Python
def counter():
    n = 0
    def inc():
        nonlocal n; n += 1; return n
    return inc

sorted_items = sorted(items, key=lambda x: x.score, reverse=True)
Ch. 11
args & kwargs
Flexible signatures, positional-only and keyword-only parameters.
11.1 — The full parameter grammar
Python
def f(pos, /, normal, *args, kw_only, **kwargs):
    ...
# pos: positional-only  (before /)
# kw_only: keyword-only (after *)

def wrap(*args, **kwargs):
    return target(*args, **kwargs)   # transparent pass-through
Ch. 12
Classes
OOP, dataclasses, properties.
12.1 — A dataclass beats boilerplate
Python
from dataclasses import dataclass, field

@dataclass(frozen=True, slots=True)
class Point:
    x: float
    y: float = 0.0
    tags: list = field(default_factory=list)

p = Point(1, 2)   # __init__, __repr__, __eq__ generated
@dataclass(slots=True) removes the per-instance __dict__ — less memory and faster attribute access. frozen=True makes instances hashable and immutable.
12.2 — Properties
Python
class C:
    @property
    def area(self): return self._w * self._h
    @area.setter
    def area(self, v): raise AttributeError("read-only")
Ch. 13
Dunder Methods
The protocol methods that make your objects feel built-in.
13.1 — The ones worth implementing
Python
class Vec:
    def __init__(self, x, y): self.x, self.y = x, y
    def __repr__(self): return f"Vec({self.x}, {self.y})"
    def __add__(self, o): return Vec(self.x+o.x, self.y+o.y)
    def __eq__(self, o): return (self.x, self.y) == (o.x, o.y)
    def __len__(self): return 2
Ch. 14
Decorators
Functions that wrap functions — caching, timing, registration.
14.1 — Writing one correctly
Python
import functools, time

def timed(fn):
    @functools.wraps(fn)        # preserve name/docstring
    def wrapper(*a, **kw):
        t = time.perf_counter()
        r = fn(*a, **kw)
        print(fn.__name__, time.perf_counter()-t)
        return r
    return wrapper

@functools.lru_cache(maxsize=None)   # memoise
def fib(n): return n if n < 2 else fib(n-1)+fib(n-2)
Ch. 15
Iterators & Generators
Lazy evaluation — the key to processing data bigger than memory.
15.1 — yield and the iterator protocol
Python
def read_chunks(path, size=8192):
    with open(path, "rb") as f:
        while (b := f.read(size)):
            yield b      # pauses here, resumes on next()

import itertools as it
it.islice(stream, 10)        # first 10 without materialising
it.chain(a, b); it.groupby(xs)
Generators turn an O(n)-memory pipeline into O(1). Chaining itertools functions lets you process a multi-GB stream with a flat memory profile.
Ch. 16
Context Managers
Deterministic cleanup with the with statement.
16.1 — with, and writing your own
Python
with open("f.txt") as f, lock:   # multiple, nested
    data = f.read()

from contextlib import contextmanager
@contextmanager
def timer():
    t = time.perf_counter()
    try: yield
    finally: print(time.perf_counter()-t)
Ch. 17
Exceptions
EAFP, exception chaining, and the new exception groups.
17.1 — Ask forgiveness, not permission
Python
try:
    v = cache[key]
except KeyError:
    v = compute(); cache[key] = v
except (IOError, ValueError) as e:
    raise AppError("failed") from e   # chain the cause
else:
    commit()      # ran only if no exception
finally:
    release()
Pythonic style is EAFP — try the operation and handle the exception — rather than LBYL (checking with if first), which races and is slower on the happy path.
Ch. 18
Modules & Packaging
Imports, __main__, and shipping a package.
18.1 — Imports and the script guard
Python
from pkg.mod import thing      # absolute (preferred)
from . import sibling           # explicit relative

if __name__ == "__main__":
    main()   # runs only when executed, not imported
A modern package needs only a pyproject.toml. Build with python -m build, publish with twine. The old setup.py is no longer required.
Ch. 19
Standard Library
Reach for these before you pip install anything.
19.1 — The batteries
ModuleUse it for
pathlibfilesystem paths (replaces os.path)
collectionsCounter, defaultdict, deque, namedtuple
itertoolslazy iterator algebra
functoolslru_cache, partial, reduce, wraps
json / csvdata interchange
datetime / zoneinfotime, with real timezones
dataclassesvalue objects without boilerplate
concurrent.futuresthread/process pools
argparseCLI argument parsing
Ch. 20
Typing & Tooling
Type hints, and the tools that keep a codebase honest.
20.1 — Hints are checked by tools, not the runtime
Python
def parse(raw: str) -> dict[str, int] | None:
    ...

from typing import Protocol
class Reader(Protocol):       # structural typing
    def read(self, n: int) -> bytes: ...
Standard toolchain: ruff (lint + format, replaces black/flake8/isort), mypy or pyright (type-check), pytest (test). Run all three in CI.
REF
Idiom Cheatsheet
Fluent Python in one screen.
Patterns
Python
a, b = b, a                      # swap
"".join(parts)                  # build strings (never +=)
for i, x in enumerate(xs): ...   # index + value
for a, b in zip(xs, ys): ...    # parallel iterate
x = a if cond else b           # ternary
val = d.get(k, default)
sorted(xs, key=lambda r: r.ts)
with open(p) as f: ...          # always context-manage I/O