Programming foundations: Python

Sefism.

Chapter 03

Python essentials

The idioms that make Python code look like Python: comprehensions, generators, type hints, dataclasses and the traps every beginner meets once.

Comprehensions

Also called list comprehension, dict comprehension

What
A compact expression that builds a list, dictionary or set by transforming and filtering another iterable, in one line.
Why
They replace a four-line loop that appends to an empty list, and they read like the sentence "the square of each n where n is even".
Python
squares = [n * n for n in range(10)]
evens = [n for n in nums if n % 2 == 0]
by_id = {user.id: user for user in users}
domains = {email.split("@")[1] for email in emails}

Watch out

Nesting two or three comprehensions with conditions; once it needs a second read, write a normal loop.

Generators and yield

Also called iterators, lazy evaluation, generator expression

What
A function that uses yield produces values one at a time, pausing between them, instead of building a whole list in memory.
Why
Generators process files or data streams far larger than memory, and they let you chain steps (read, filter, transform) without temporary lists.
Python
def non_empty_lines(path: str):
    with open(path, encoding="utf-8") as f:
        for line in f:
            if line.strip():
                yield line.rstrip("\n")

total = sum(len(line) for line in non_empty_lines("big.log"))

Watch out

Looping over the same generator twice: it is used up after the first pass and the second loop sees nothing.

Truthiness and is None

Also called falsy values, is vs ==

What
In a condition, empty collections, 0, "" and None count as false and almost everything else as true. is checks whether two names are the same object; == checks whether values are equal.
Why
if not items: is the idiomatic empty check. is None is the correct way to test for a missing value.
Python
items = []
if not items:
    print("empty")

count = 0
if count is not None:
    print("count was given, even though it is 0")

Watch out

Writing if count: when 0 is a valid value, so a real zero is treated as "not given".

Mutable default arguments

What
Default values are created once, when the function is defined, not on every call. A default list or dictionary is therefore shared by every call that uses it.
Why
It is the classic Python surprise: data leaks between calls. Knowing the rule tells you to use None as the default and create the list inside.
Python
def add_tag(tag, tags=[]):
    tags.append(tag)
    return tags

add_tag("a")
print(add_tag("b"))  # ['a', 'b']

def add_tag_fixed(tag, tags=None):
    tags = [] if tags is None else tags
    tags.append(tag)
    return tags

Watch out

Writing def f(items=[]) or def f(config={}) and appending to it.

Type hints

Also called type annotations, mypy, Pyright, typing

What
Annotations that state the expected types of parameters, return values and variables. Python ignores them at runtime; checkers such as mypy and Pyright, and your editor, read them.
Why
Hints catch wrong-type bugs before you run the code, power autocomplete and document what a function expects. Python 3.12 added the short def f[T] generic syntax.
Python
from collections.abc import Iterable

type UserId = int

def first[T](items: list[T]) -> T | None:
    return items[0] if items else None

def total(prices: Iterable[float]) -> float:
    return sum(prices)

Watch out

Assuming hints validate data at runtime: a function typed int still accepts a string unless a checker or a library such as Pydantic enforces it.

Dataclasses

Also called @dataclass

What
A decorator that writes __init__, __repr__ and __eq__ for a class from its annotated fields.
Why
Use one for records of data (a task, an order line) instead of a dictionary with string keys: you get typo-proof attributes, readable printing and equality for free.
Python
from dataclasses import dataclass, field

@dataclass
class Task:
    title: str
    done: bool = False
    tags: list[str] = field(default_factory=list)

task = Task("write tests")
print(task)  # Task(title='write tests', done=False, tags=[])

Watch out

Giving a field a mutable default like tags: list = []; use field(default_factory=list).

The main guard

Also called if __name__ == "__main__", entry point

What
The check if __name__ == "__main__": is true only when the file is run directly, not when another file imports it.
Why
It lets a module be both a runnable script and an importable library, so tests can import your functions without running the whole program.
Python
def main() -> None:
    print("running as a script")

if __name__ == "__main__":
    main()

Watch out

Putting program logic at the top level of a module, so importing it for a test starts reading input or writing files.

03 · Python essentials3 / 10