Python
from first principles
A complete syntax reference built for Mac users — from installing Python with Homebrew through advanced language features, with every example runnable in your Terminal.
00 — Setting Up Python on Your Mac
macOS ships with Python 3 but it's often outdated. The best path: install Homebrew (the Mac package manager), use it to install pyenv (Python version manager), then install the latest Python. Takes about 5 minutes.
Install Homebrew
Homebrew is the missing package manager for macOS. Open Terminal (⌘ Space → "Terminal") and run the one-liner from brew.sh.
Install pyenv
pyenv lets you install and switch between multiple Python versions. Essential for real projects. Install via Homebrew.
Install Python
Use pyenv to install the latest stable Python. Set it as your global default. Confirm with python3 --version.
Pick an Editor
VS Code (free, excellent Python extension) or PyCharm CE (free, Python-first IDE). Both are in Homebrew: brew install --cask visual-studio-code.
python3 -m venv .venv creates an isolated environment.source .venv/bin/activate activates it — your terminal prompt gains (.venv).deactivate exits it. This keeps every project's packages separate.
Variables & Types
Python is dynamically typed — you assign a value and Python infers the type. No int x = 0 declarations. Types are objects: int, float, bool, str, None, list, dict, tuple, set.
No declaration keyword
Write name = "Alice" — no var, let, or int. The type is attached to the value, not the variable. The same variable can hold different types at different times (though doing so is bad practice).
Boolean literals are capitalised
True and False — not true/false. Python's null is None, not null or nil.
Type system
Truthiness
Everything is truthy except: None, False, 0, 0.0, "", [], {}, ().
So if my_list: means "if the list is non-empty" — idiomatic Python.
Type hints (Python 3.5+)
Hints are documentation for tools (mypy, your IDE). Python ignores them at runtime. Write def greet(name: str) -> str: to document intent without enforcing it.
Strings & f-strings
Strings are immutable sequences of Unicode characters. Index them like a list. Slice them. Iterate over them. Python 3.6+ f-strings are the best way to embed values in text.
Three quoting styles
'single' and "double" are identical — use whichever avoids escaping. """triple""" spans multiple lines and is also used for docstrings.
f-strings — the preferred formatting method
f"Hello, {name}!" evaluates the expression inside {} and converts it to a string. You can put any expression inside: f"{2 + 2}", f"{obj.method()}".
Format specifiers go after a colon: f"{value:.2f}" → 2 decimal places. f"{n:05d}" → zero-padded 5-digit int.
Slicing — [start:stop:step]
All three parts are optional. Negative indices count from the end: s[-1] is the last character. s[::-1] reverses the string.
Raw strings
Prefix r: r"C:\Users\Alice" — backslashes are literal. Essential for file paths on Windows and for regex patterns.
Essential string methods
- .strip()Remove leading/trailing whitespace. .lstrip() / .rstrip() for one side.
- .split()Split on whitespace (default) or delimiter. Returns a list.
- .join()Opposite of split: ", ".join(["a","b","c"]) → "a, b, c"
- .replace()Replace all occurrences: "hello".replace("l","r") → "herro"
- .startswith()Returns bool. Useful for if line.startswith("#"):
- .upper() .lower()Case conversion. .title() for Title Case.
Control Flow
Python uses indentation (4 spaces) instead of braces {}. There are no semicolons. Colons (:) end condition lines. Python 3.10+ adds match/case.
Indentation IS syntax
Unlike C, Java, or JavaScript, indentation is not optional. Inconsistent indentation raises IndentationError. The standard is 4 spaces — configure your editor to insert spaces on Tab.
for loops iterate directly
Python's for loop iterates over any iterable — lists, strings, dicts, generators. You rarely need an index counter. When you do, use enumerate().
range(start, stop, step)
Generates integers lazily. range(10) → 0–9. range(2, 10, 2) → 2, 4, 6, 8. range(10, 0, -1) → 10 down to 1.
for...else — a Python exclusive
The else block on a for loop runs only if the loop completed without hitting a break. Useful for "search and report not found" patterns.
match / case (Python 3.10+)
Structural pattern matching. More powerful than a switch statement — it can match values, types, sequences, and structures with guard clauses (if). Wildcard: case _.
Functions
Functions are first-class objects — they can be stored in variables, passed as arguments, and returned from other functions. Python supports default arguments, keyword arguments, variadic arguments, and closures.
Defining a function
def name(params): ... — the body must be indented. return exits with a value. A function without return returns None.
Default arguments
def greet(name, greeting="Hello"): — parameters with defaults are optional. Critical rule: default arguments are evaluated once at function definition, not each call. Never write def f(x, items=[]): — that list is shared across all calls.
*args and **kwargs
- *argsCollects extra positional arguments into a tuple. def total(*nums): return sum(nums)
- **kwargsCollects extra keyword arguments into a dict. def config(**opts): ...
- *A bare * in the signature makes all following params keyword-only: def f(a, *, b): — b must be named at call site.
lambda — anonymous functions
lambda x: x * 2 is a single-expression function. Useful as a key= argument to sorted()/max() etc. Prefer a named def for anything more complex.
Closures
An inner function that references a variable from its enclosing scope creates a closure — the variable is captured. Used to build factory functions.
Lists & Tuples
list — ordered, mutable, allows duplicates. tuple — ordered, immutable, allows duplicates. Use tuples for records whose fields won't change; lists for collections you'll modify.
List methods
sorted() vs .sort()
sorted(lst) returns a new sorted list, leaving the original unchanged. lst.sort() sorts in-place and returns None. Prefer sorted() when you want to keep the original.
Tuple unpacking
x, y = (3, 4) — assigns each element to a variable. first, *rest = [1,2,3,4] uses the starred expression to collect the tail. Useful for swapping: a, b = b, a.
namedtuple — a lightweight struct
from collections import namedtuple
Point = namedtuple("Point", ["x","y"])
Access by name: p.x and by index: p[0]. Immutable like a regular tuple.
Dicts & Sets
dict — unordered key→value mapping (ordered by insertion since Python 3.7). set — unordered collection of unique hashable values. Both have O(1) average lookup.
Dict access patterns
- d[key]Direct access — raises KeyError if key absent. Use when you expect the key to exist.
- .get(k,v)Safe access — returns None (or default v) if key missing. Never raises. Preferred for uncertain keys.
- .setdefault(k,v)Returns value if key exists; otherwise inserts v and returns it. Useful for building dicts of lists.
- k in dMembership test — O(1). Tests keys only. k in d.values() is O(n).
- .items()Returns view of (key, value) pairs. Use in for k, v in d.items():
Dict merging (Python 3.9+)
merged = dict1 | dict2 creates a new merged dict. dict1 |= dict2 updates in-place. Before 3.9: {**dict1, **dict2}.
Set operations
defaultdict
from collections import defaultdict
dd = defaultdict(list) — accessing a missing key auto-creates it with list() as the default value. Great for grouping: dd["key"].append(item) without checking if the key exists first.
Comprehensions
The most distinctively Pythonic syntax. Build lists, dicts, and sets in one expressive line. Comprehensions are faster than equivalent for loops and are the preferred way to transform or filter collections.
The pattern
[expression for item in iterable if condition]
All three parts: the expression transforms each item, the for iterates, and the if filters. The if clause is optional.
Four flavours
Generator expressions save memory
sum(x**2 for x in range(1_000_000)) — the generator produces values one at a time without building a million-element list. Use generators inside function calls like sum(), max(), any(), all().
Nested comprehension — flattening
[x for row in matrix for x in row] — two for clauses flatten a 2D list. Read left to right as nested loops: outer loop first, inner second.
Classes & OOP
Everything in Python is an object — integers, strings, functions, modules. You define your own types with class. The first argument to every method is self (the instance), passed automatically.
__init__ — the initialiser
Called when you write Dog("Rex", "Labrador"). Sets up instance variables on self. It is not a constructor — the object is already created when __init__ runs.
Instance vs class variables
@property
Turns a method into an attribute: callers write dog.weight instead of dog.weight(). Add a setter with @weight.setter to validate on assignment.
@classmethod vs @staticmethod
- @classmethodFirst arg is cls (the class). Use as alternate constructors: Dog.from_dict(data).
- @staticmethodNo implicit first arg. Just a function attached to the class for organisation. Doesn't know about self or cls.
@dataclass — zero-boilerplate classes
Automatically generates __init__, __repr__, and __eq__. Write x: type = default for each field. Use field(default_factory=list) for mutable defaults.
Key dunder methods
Inheritance
Python supports single and multiple inheritance. super() calls the parent class. Python uses the C3 linearisation algorithm (MRO) to resolve method order in multiple inheritance.
Subclassing
class Dog(Animal): — Dog inherits all methods and class variables from Animal. Override by defining the same method name in the subclass.
super() — calling the parent
super().__init__(name, sound) calls Animal.__init__. Without super(), the parent's __init__ is skipped and instance variables won't be set.
isinstance vs type()
isinstance(rex, Animal) returns True for instances of Animal and any subclass. type(rex) is Animal returns False for subclass instances. Almost always prefer isinstance().
Abstract base classes
from abc import ABC, abstractmethod — a class inheriting ABC can declare @abstractmethod methods that subclasses must implement. Instantiating the ABC directly raises TypeError.
Multiple inheritance & MRO
class C(A, B): — Python resolves method lookup in a consistent left-to-right order (C3). Inspect with C.__mro__. Keep multiple inheritance shallow; prefer composition for complex cases.
Exception Handling
Python uses exceptions for all error handling. The full syntax is try / except / else / finally. Catch the most specific exceptions first. Never use bare except: without a type.
try / except / else / finally
- tryThe block that might raise an exception.
- exceptCatches a specific exception type. List multiple types in a tuple: except (TypeError, ValueError):
- elseRuns only if the try block succeeded (no exception). Cleaner than putting success code in try.
- finallyAlways runs — cleanup code (close files, release locks). Runs even if an exception was raised and not caught.
raise — throwing exceptions
raise ValueError("must be positive") — raises with a message. Plain raise inside an except block re-raises the current exception.
Custom exceptions
Inherit from Exception (or a more specific built-in). Add custom attributes in __init__. Use a hierarchy for your app: class AppError(Exception) as the base, then class NotFoundError(AppError).
• except: with no type — catches even SystemExit and KeyboardInterrupt.
• except Exception: everywhere — hides bugs.
• Empty except blocks — silently swallows errors.
Common exceptions
| Type | Raised when |
|---|---|
| ValueError | Right type, wrong value: int("abc") |
| TypeError | Wrong type: "a" + 1 |
| KeyError | Dict key missing: d["x"] |
| IndexError | List index out of range |
| AttributeError | Object has no such attribute |
| FileNotFoundError | File doesn't exist |
| ZeroDivisionError | 10 / 0 |
| StopIteration | Generator/iterator exhausted |
File I/O
Always use the with context manager — it guarantees the file is closed even if an exception occurs. Use pathlib.Path for path manipulation instead of string concatenation.
File modes
pathlib — the modern way
Use Path instead of string concatenation for paths. The / operator joins paths: Path.home() / "Documents" / "data.txt" — this works on Mac, Linux, and Windows.
Modules & Packages
Any .py file is a module. A directory with __init__.py is a package. Always use virtual environments — one per project — to keep dependencies isolated.
Import styles
Virtual environments on Mac
Package structure
myproject/ ├── .venv/ ├── mypackage/ │ ├── __init__.py ← makes it a package │ ├── utils.py │ └── models/ │ ├── __init__.py │ └── user.py ├── main.py └── requirements.txt
__name__ guard — always use it
if __name__ == "__main__": — code inside only runs when the file is executed directly, not when imported as a module. Without it, importing your module would execute its side effects.
Decorators
A decorator is a function that wraps another function. The @ syntax is shorthand: @timer above def f() is exactly f = timer(f). Used for logging, caching, auth, timing, and more.
Anatomy of a decorator
Three layers: the outer function receives the function to wrap, the inner wrapper adds before/after logic and calls the original, and the outer function returns wrapper.
@functools.wraps — always use it
Without @functools.wraps(func), your decorator replaces func.__name__ and func.__doc__ with "wrapper". @wraps copies the original's metadata onto the wrapper, preserving tracebacks and documentation.
Decorators with arguments
Add another level: the outermost function takes the arguments and returns the actual decorator. @retry(times=3) requires: outer function retry(times) → returns decorator → returns wrapper.
Stacking decorators
Decorators are applied bottom-up. @a above @b above def f means f = a(b(f)).
@functools.lru_cache
Built-in memoisation decorator. Caches function results by arguments. @lru_cache(maxsize=128) — ideal for recursive functions like Fibonacci. @cache (Python 3.9+) is lru_cache with unlimited size.
Generators & Context Managers
yield turns a function into a generator — values are produced lazily, one at a time. Context managers (with) manage resources cleanly using __enter__ / __exit__.
How generators work
When Python sees yield, calling the function returns a generator object without executing any code. Each next() call runs until the next yield, then suspends. When the function returns, StopIteration is raised.
yield from — delegating
yield from other_iterable delegates to another iterable, yielding each of its values. Cleaner than a nested for loop with yield.
Context managers
The with statement calls __enter__ on entry and __exit__ on exit — even if an exception occurs. Implement with a class or with @contextlib.contextmanager.
@contextmanager — the easy way
Write a generator function with a single yield inside a try/finally. Code before yield is __enter__; code after is __exit__. The yielded value becomes the as target.
itertools — lazy combinatorics
Essential Standard Library
Python ships with "batteries included". These modules cover the most common needs without any pip install.
Most-used modules
| Module | Use for |
|---|---|
| datetime | Dates, times, timedeltas, formatting |
| pathlib | File paths — use instead of os.path |
| os | Environment vars, os.getcwd(), os.makedirs() |
| sys | sys.argv, sys.exit(), sys.path |
| json | Parse/serialise JSON data |
| csv | Read/write CSV files |
| re | Regular expressions |
| collections | Counter, defaultdict, deque, namedtuple |
| itertools | Lazy iteration combinators |
| functools | wraps, lru_cache, partial, reduce |
| random | Random numbers, choices, shuffling |
| math | Maths functions, math.pi, math.inf |
| subprocess | Run shell commands from Python |
| logging | Structured log output (use instead of print) |
| threading / asyncio | Concurrency — threads or async I/O |
| sqlite3 | Built-in SQL database |
| hashlib | MD5, SHA-256 hashing |
| urllib / http | HTTP requests (or use httpx/requests) |
Type Hints & Annotations
Type hints are optional documentation that power your IDE's autocompletion and let mypy catch bugs before runtime. Python ignores them at runtime — they are purely for tooling and human readers.
Why type hints matter on Mac
Install mypy with pip install mypy and run mypy myfile.py. VS Code's Pylance extension shows type errors inline as you type — both catch bugs before you run code.
Basic annotation syntax
name: str = "Alice" — annotate a variable. def greet(n: str) -> str: — annotate parameters and return type. Neither changes runtime behaviour.
Key types from typing
Version shortcuts
- 3.9+Built-in generics: list[str], dict[str,int], tuple[int,...]
- 3.10+Union with pipe: str | None instead of Optional[str]
- 3.11+Self type for methods returning the instance
- 3.12+type X = list[int] — new type alias syntax
Async / Await & asyncio
Asynchronous programming lets a single thread handle many I/O operations concurrently. Use it for network requests, database queries, and anything that spends time waiting. asyncio is the standard event loop.
Concurrency vs parallelism
asyncio is concurrent but single-threaded — perfect for I/O-bound work (HTTP calls, database, file reads). For CPU-bound work, use multiprocessing or concurrent.futures.ProcessPoolExecutor.
The mental model
await means: "suspend this coroutine and let the event loop run other tasks while we wait." The event loop resumes this coroutine when the result is ready. Nothing blocks the thread.
Key asyncio functions
async for / async with
Async context managers (async with) and iterators (async for) work inside async def functions. Used by aiohttp, httpx, aiofiles, and database async drivers.
Testing with unittest & pytest
Testing is how you prove your code works. Python ships with unittest; pytest is simpler, more powerful, and the community standard. Both run from Terminal with a single command.
unittest — in the stdlib
Subclass unittest.TestCase. Test methods start with test_. Use assertion methods like self.assertEqual(a, b), self.assertRaises(ValueError, fn, arg). Run: python3 -m unittest.
pytest — the recommended choice
pip install pytest. Write plain functions starting with test_ — no class needed. Use bare assert statements — pytest rewrites them to show detailed diffs on failure. Run: pytest -v.
pytest features
- @fixtureFunctions that provide setup/teardown, injected by parameter name. Use yield for teardown code.
- @parametrizeRun the same test with multiple inputs: @pytest.mark.parametrize("a,b,exp", [(1,2,3),...])
- tmp_pathBuilt-in fixture: a fresh temporary Path per test, auto-deleted after.
- monkeypatchBuilt-in fixture: temporarily replace functions, env vars, attributes.
- capsysBuilt-in fixture: capture and assert on stdout/stderr.
- raiseswith pytest.raises(ValueError): — assert a specific exception is raised.
Mocking with unittest.mock
from unittest.mock import MagicMock, patch — replace real dependencies with fakes during a test. Use @patch("module.name") as a decorator for one test, or with patch(...) as mock: as a context manager.
Quick Reference
All 18 sections at a glance — key syntax and concepts to remember.
Homebrew → pyenv → Python 3.12. Always use venv. source .venv/bin/activate.
No declarations. True/False/None capitalised. isinstance(x, int). Type hints are optional documentation.
Immutable sequences. f"{expr}" preferred. Slice with [start:stop:step]. Raw strings: r"...".
Indentation = syntax. for x in iterable. enumerate(). match/case (3.10+). for/else.
First-class objects. Default args. *args/**kwargs. lambda x: expr. Closures capture outer scope.
List: mutable. Tuple: immutable. sorted(lst) vs lst.sort(). Unpack: a, *rest = lst.
Dict: O(1) lookup. .get(k, default). Set: unique values. set() for empty. | merges (3.9+).
[e for x in it if c]. Dict: {k:v …}. Set: {e …}. Generator: (e …) — lazy.
__init__(self). @property. @classmethod/@staticmethod. @dataclass. Dunder methods.
class Dog(Animal). super().__init__(). isinstance() prefers over type(). Abstract base classes.
try/except/else/finally. Catch specific types. raise ValueError("msg"). Never swallow silently.
with open(p, "r", encoding="utf-8"). pathlib.Path. json.load/dump. Always UTF-8.
One venv per project. pip install. requirements.txt. if __name__ == "__main__".
@name = f = name(f). Always use @functools.wraps. @lru_cache for memoisation.
yield = lazy values. yield from delegates. with = context manager. @contextmanager.
▶ PEP 8 — The Python Style Guide (key rules for Mac editor setup)
| Rule | Detail | Mac editor tip |
|---|---|---|
| Indentation | 4 spaces — never tabs | VS Code: ⌘, → Tab Size: 4, Insert Spaces: on |
| Line length | 79 chars max (99 for teams) | VS Code: set "editor.rulers": [79] |
| Naming | snake_case vars/funcs, PascalCase classes, UPPER_CASE constants | — |
| Blank lines | 2 between top-level defs, 1 between methods | — |
| Imports | stdlib, then third-party, then local. Alphabetical within groups. | Install isort extension |
| Quotes | Consistent single or double — your choice | Black auto-formats to double |
| Auto-format | Run black or ruff format | brew install black then black *.py |
▶ Operator reference
| Arithmetic | Result |
|---|---|
| 10 / 3 | 3.333… (always float) |
| 10 // 3 | 3 (floor division) |
| 10 % 3 | 1 (modulo) |
| 2 ** 8 | 256 (exponentiation) |
| Logic/Comparison | Notes |
|---|---|
| and / or / not | not && || ! |
| == / != | value equality |
| is / is not | identity — use with None |
| in / not in | membership test |