Beginner~20 min read

Python

A modern tour of Python 3.12+ covering syntax, data structures, OOP, type hints, async/await, the standard library, and tooling.

SyntaxData StructuresAsyncTyping

Python is a high-level, dynamically typed, garbage-collected language prized for its readable syntax and enormous ecosystem. It powers web backends, data science, automation, and AI tooling. These notes target Python 3.12+, which introduces a cleaner generics syntax, the type statement, and continued improvements to structural pattern matching and performance.

Zen of Python

Run import this in a REPL. "Readability counts" and "There should be one obvious way to do it" summarize Python's design philosophy.

Syntax Basics

Python uses indentation (not braces) to define blocks. Statements end at newlines; no semicolons are needed. Comments start with #.

python
def greet(name):
    if name:
        print(f"Hello, {name}!")   # f-string interpolation
    else:
        print("Hello, stranger!")

greet("Ada")

Variables and Types

Variables are names bound to objects; no declaration keyword is required. Python is dynamically but strongly typed: it never implicitly coerces incompatible types.

TypeExampleNotes
int42Arbitrary precision
float3.14IEEE 754 double
str"hi"Immutable, Unicode
boolTrue / FalseSubclass of int
NoneTypeNoneAbsence of a value
python
x = 10             # int
name = "Ada"       # str
pi = 3.14159       # float
active = True      # bool
nothing = None
a, b = 1, 2        # tuple unpacking
big = 1_000_000    # underscores for readability

Functions

Functions support positional, default, keyword-only, and variadic parameters. Use *args for extra positionals and **kwargs for extra keywords. A bare * marks following params as keyword-only.

python
def connect(host, port=5432, *, timeout=30, **options):
    print(host, port, timeout, options)

connect("db.local", 6543, timeout=10, ssl=True)

def total(*nums):
    return sum(nums)

# Lambdas: small anonymous functions
square = lambda n: n * n
sorted_words = sorted(words, key=lambda w: len(w))

Gotcha: mutable defaults

Never use a mutable default like def f(items=[]) — the list is shared across calls. Use items=None and create it inside.

Data Structures

The four core built-in containers each have distinct semantics.

TypeLiteralOrderedMutable
list[1, 2, 3]YesYes
tuple(1, 2, 3)YesNo
dict{"a": 1}InsertionYes
setNoYes
python
nums = [3, 1, 2]
nums.append(4); nums.sort()
first, *rest = nums          # unpacking with star

point = (10, 20)             # immutable pair
user = {"name": "Ada", "age": 36}
user["email"] = "ada@x.io"
age = user.get("age", 0)     # safe lookup with default

unique = {1, 2, 2, 3}        # -> {1, 2, 3}
merged = {**user, "role": "admin"}   # dict merge (or user | extra)

Comprehensions

Comprehensions build collections concisely. They exist for lists, dicts, sets, and generators.

python
squares = [n * n for n in range(10)]
evens = [n for n in range(20) if n % 2 == 0]
lookup = {word: len(word) for word in words}
uniq_lens = {len(w) for w in words}
gen = (n * n for n in range(1_000_000))   # lazy generator

Control Flow

Standard if/elif/else, for, and while loops apply. Since 3.10, structural pattern matching with match destructures data.

python
for i, item in enumerate(items):
    if item is None:
        continue
    print(i, item)

def describe(point):
    match point:
        case (0, 0):
            return "origin"
        case (x, 0):
            return f"on x-axis at {x}"
        case (x, y) if x == y:
            return "diagonal"
        case _:
            return "elsewhere"

Error Handling

Python uses exceptions. Wrap risky code in try, handle with except, run cleanup in finally, and use else for the no-error path.

python
try:
    value = int(raw)
except (ValueError, TypeError) as err:
    raise ValueError(f"bad input: {raw!r}") from err
else:
    print("parsed", value)
finally:
    print("done")

# Custom exceptions
class ConfigError(Exception):
    pass

# Context managers auto-release resources
with open("data.txt", encoding="utf-8") as f:
    text = f.read()

EAFP over LBYL

Python favors "Easier to Ask Forgiveness than Permission" — attempt the operation and catch the exception, rather than exhaustively checking preconditions.

Classes and OOP

Classes bundle state and behavior. __init__ is the constructor; self is the instance reference. Dunder methods hook into operators and built-ins.

python
class Account:
    def __init__(self, owner, balance=0):
        self.owner = owner
        self._balance = balance     # convention: _ means "internal"

    def deposit(self, amount):
        self._balance += amount

    @property
    def balance(self):
        return self._balance

    def __repr__(self):
        return f"Account({self.owner!r}, {self._balance})"

class Savings(Account):             # inheritance
    def __init__(self, owner, balance=0, rate=0.03):
        super().__init__(owner, balance)
        self.rate = rate

Dataclasses

The @dataclass decorator generates __init__, __repr__, and __eq__ from annotations, cutting boilerplate.

python
from dataclasses import dataclass, field

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

p = Point(1.0, 2.0)   # frozen -> immutable, hashable

Type Hints

Type hints are optional annotations checked by tools like mypy, not enforced at runtime. Python 3.12 added a cleaner generics syntax: declare type parameters in square brackets directly on functions and classes.

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

class Stack[T]:
    def __init__(self) -> None:
        self._items: list[T] = []
    def push(self, item: T) -> None:
        self._items.append(item)

# 3.12 type alias statement
type Vector = list[float]
type Json = dict[str, "Json"] | list["Json"] | str | int | float | bool | None

from typing import Optional, Callable, Protocol
Handler = Callable[[str, int], bool]
HintMeaning
list[int]List of ints
dict[str, int]Str keys, int values
`intNone`
Callable[..., T]Any callable returning T
ProtocolStructural (duck) typing

Async and Concurrency

Coroutines defined with async def run cooperatively on an event loop. Use await to yield control during I/O, and asyncio to schedule them. This excels at I/O-bound workloads (network, disk), not CPU-bound work.

python
import asyncio

async def fetch(url: str) -> str:
    await asyncio.sleep(1)     # simulate I/O
    return f"body of {url}"

async def main() -> None:
    # Run concurrently and gather results
    async with asyncio.TaskGroup() as tg:   # 3.11+ structured concurrency
        t1 = tg.create_task(fetch("a"))
        t2 = tg.create_task(fetch("b"))
    print(t1.result(), t2.result())

    results = await asyncio.gather(fetch("x"), fetch("y"))

asyncio.run(main())

GIL and parallelism

The Global Interpreter Lock means threads don't run Python bytecode in parallel. For CPU-bound work use multiprocessing or ProcessPoolExecutor; async and threads only help I/O-bound work.

Standard Library Highlights

Python ships "batteries included." A handful of modules cover most day-to-day needs.

ModuleUse
collectionsCounter, defaultdict, deque, namedtuple
itertoolschain, groupby, product, combinations
pathlibObject-oriented filesystem paths
jsonEncode/decode JSON
functoolscache, reduce, partial, wraps
datetimeDates, times, timezones
python
from collections import Counter, defaultdict
from pathlib import Path
import json, itertools

Counter("mississippi").most_common(1)      # [('i', 4)]
groups = defaultdict(list)
config = json.loads(Path("config.json").read_text())
pairs = list(itertools.product([1, 2], ["a", "b"]))

Tooling

Isolate dependencies per project and lean on fast, modern tooling for formatting, type-checking, and testing.

ToolPurpose
pip / venvInstall packages into isolated environments
uvUltra-fast installer, resolver, and env manager
ruffLightning-fast linter and formatter
mypyStatic type checker
pytestTest framework
bash
python -m venv .venv && source .venv/bin/activate
pip install requests
uv add requests          # modern alternative
ruff check . && ruff format .
mypy src/
pytest -q

Practice Exercises

  1. Write a function that takes *args of numbers and returns the mean, handling the empty case by raising a custom EmptyInputError.
  2. Given a list of words, build a dict mapping each word to its length using a dict comprehension, then filter to words longer than 4 letters.
  3. Create a frozen @dataclass named Money with amount and currency fields, and implement addition of two same-currency instances.
  4. Write a generic function def chunk[T](items: list[T], size: int) -> list[list[T]] that splits a list into fixed-size chunks.
  5. Use asyncio.gather to fetch three simulated URLs concurrently and print the total elapsed time to prove they ran in parallel.
  6. Use collections.Counter and pathlib to read a text file and print the ten most common words, then add a pytest test for it.

Section navigation