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 #.
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.
| Type | Example | Notes |
|---|---|---|
int | 42 | Arbitrary precision |
float | 3.14 | IEEE 754 double |
str | "hi" | Immutable, Unicode |
bool | True / False | Subclass of int |
NoneType | None | Absence of a value |
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.
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.
| Type | Literal | Ordered | Mutable |
|---|---|---|---|
| list | [1, 2, 3] | Yes | Yes |
| tuple | (1, 2, 3) | Yes | No |
| dict | {"a": 1} | Insertion | Yes |
| set | No | Yes |
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.
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.
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.
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.
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.
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.
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]
| Hint | Meaning |
|---|---|
list[int] | List of ints |
dict[str, int] | Str keys, int values |
| `int | None` |
Callable[..., T] | Any callable returning T |
Protocol | Structural (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.
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.
| Module | Use |
|---|---|
collections | Counter, defaultdict, deque, namedtuple |
itertools | chain, groupby, product, combinations |
pathlib | Object-oriented filesystem paths |
json | Encode/decode JSON |
functools | cache, reduce, partial, wraps |
datetime | Dates, times, timezones |
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.
| Tool | Purpose |
|---|---|
pip / venv | Install packages into isolated environments |
uv | Ultra-fast installer, resolver, and env manager |
ruff | Lightning-fast linter and formatter |
mypy | Static type checker |
pytest | Test framework |
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
- Write a function that takes
*argsof numbers and returns the mean, handling the empty case by raising a customEmptyInputError. - 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.
- Create a frozen
@dataclassnamedMoneywith amount and currency fields, and implement addition of two same-currency instances. - Write a generic function
def chunk[T](items: list[T], size: int) -> list[list[T]]that splits a list into fixed-size chunks. - Use
asyncio.gatherto fetch three simulated URLs concurrently and print the total elapsed time to prove they ran in parallel. - Use
collections.Counterandpathlibto read a text file and print the ten most common words, then add a pytest test for it.