Control Flow#
Conditionals — Beyond Basics#
# Walrus operator (Python 3.8+) — assign and test in one expression
if (n := len(data)) > 100:
print(f"Processing {n} items in batches")
# Chained comparisons
if 0 <= score <= 100:
print("Valid score")
# Match statement (Python 3.10+) — structural pattern matching
def handle_command(command):
match command.split():
case ["quit"]:
return "Goodbye"
case ["hello", name]:
return f"Hello, {name}!"
case ["add", *numbers] if all(n.isdigit() for n in numbers):
return sum(int(n) for n in numbers)
case _:
return "Unknown command"
# Pattern matching with objects
match event:
case {"type": "click", "x": x, "y": y}:
handle_click(x, y)
case {"type": "keypress", "key": key}:
handle_key(key)
case {"type": "scroll", "direction": "up" | "down" as direction}:
handle_scroll(direction)
Iteration Patterns#
# zip — iterate multiple sequences in parallel
names = ["Alice", "Bob", "Charlie"]
scores = [95, 87, 92]
for name, score in zip(names, scores):
print(f"{name}: {score}")
# zip_longest — don't stop at shortest
from itertools import zip_longest
for a, b in zip_longest([1, 2, 3], [10, 20], fillvalue=0):
print(a, b) # (1,10), (2,20), (3,0)
# enumerate with start
for i, line in enumerate(lines, start=1):
print(f"Line {i}: {line}")
# reversed and sorted
for item in reversed(items):
process(item)
for item in sorted(items, key=lambda x: x.priority, reverse=True):
process(item)
# Dictionary iteration patterns
for key in sorted(config.keys()):
print(f"{key} = {config[key]}")
# Nested loop with product
from itertools import product
for x, y in product(range(3), range(3)):
print(f"({x}, {y})")
Comprehensions — Advanced#
# Nested comprehension (flatten)
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
flat = [x for row in matrix for x in row] # [1, 2, 3, 4, 5, 6, 7, 8, 9]
# Conditional expression in comprehension
labels = ["PASS" if score >= 60 else "FAIL" for score in scores]
# Dictionary comprehension with filtering
env_vars = {k: v for k, v in os.environ.items() if k.startswith("APP_")}
# Set comprehension
unique_domains = {email.split("@")[1] for email in emails}
# Walrus in comprehension (filter + transform)
results = [cleaned for raw in data if (cleaned := clean(raw)) is not None]
Functions — Advanced Patterns#
Parameter Types#
def complex_function(
required_arg, # positional or keyword
*args, # variable positional
keyword_only, # must be passed by name
default_kwarg="default", # keyword with default
**kwargs # variable keyword
):
pass
# Positional-only parameters (Python 3.8+)
def divmod(a, b, /): # a and b MUST be positional
return a // b, a % b
# Combined
def api_call(url, /, *, method="GET", headers=None, **params):
# url is positional-only
# method, headers are keyword-only
# params catches extra keyword args
pass
Closures and Factories#
def make_validator(min_val, max_val, required=True):
"""Factory: creates a validation function with captured config."""
def validate(value):
if value is None:
return not required
return min_val <= value <= max_val
return validate
validate_age = make_validator(0, 150)
validate_score = make_validator(0, 100)
validate_optional_rating = make_validator(1, 5, required=False)
validate_age(25) # True
validate_age(200) # False
validate_optional_rating(None) # True
Partial Application#
from functools import partial
def power(base, exponent):
return base ** exponent
square = partial(power, exponent=2)
cube = partial(power, exponent=3)
square(5) # 25
cube(3) # 27
# Practical: configure a logger
import logging
debug = partial(logging.log, logging.DEBUG)
error = partial(logging.log, logging.ERROR)
debug("This is a debug message")
Function Annotations and Overloading#
from typing import overload
@overload
def process(data: str) -> list[str]: ...
@overload
def process(data: list[str]) -> str: ...
def process(data):
if isinstance(data, str):
return data.split()
elif isinstance(data, list):
return " ".join(data)
Modules and Packages#
Module System#
# Every .py file is a module
# __name__ is "__main__" when run directly, module name when imported
# my_module.py
def helper():
return "I'm a helper"
_private_var = "not exported by convention"
if __name__ == "__main__":
# Only runs when executed directly, not when imported
print(helper())
Package Structure#
my_package/
├── __init__.py # Makes it a package, runs on import
├── core.py
├── utils/
│ ├── __init__.py
│ ├── strings.py
│ └── numbers.py
└── _internal.py # Convention: private module
# __init__.py — control what's exported
from .core import MainClass, main_function
from .utils.strings import clean_text
__all__ = ["MainClass", "main_function", "clean_text"]
# Controls what `from my_package import *` exports
Import Mechanics#
# Python searches for modules in this order:
# 1. sys.modules cache (already imported)
# 2. Built-in modules
# 3. sys.path (current dir, PYTHONPATH, site-packages)
import sys
print(sys.path) # shows search order
# Lazy imports (defer heavy imports)
def process_image(path):
from PIL import Image # only imported when function is called
return Image.open(path)
# Conditional imports
try:
import ujson as json # faster JSON library
except ImportError:
import json # fallback to stdlib
Hypothetical Use Cases#
Use Case: CLI Argument Parser#
def parse_args(argv: list[str]) -> dict:
"""Simple CLI argument parser using pattern matching."""
args = {"verbose": False, "output": "stdout", "files": []}
i = 0
while i < len(argv):
match argv[i]:
case "-v" | "--verbose":
args["verbose"] = True
case "-o" | "--output":
i += 1
args["output"] = argv[i]
case arg if not arg.startswith("-"):
args["files"].append(arg)
case unknown:
raise ValueError(f"Unknown argument: {unknown}")
i += 1
return args
Use Case: Plugin System#
import importlib
from pathlib import Path
def load_plugins(plugin_dir: str) -> dict:
"""Dynamically load all plugins from a directory."""
plugins = {}
for path in Path(plugin_dir).glob("*.py"):
if path.name.startswith("_"):
continue
module_name = path.stem
spec = importlib.util.spec_from_file_location(module_name, path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
# Each plugin must have a register() function
if hasattr(module, "register"):
plugin_info = module.register()
plugins[plugin_info["name"]] = module
return plugins
Key Takeaways#
- Pattern matching (3.10+) replaces complex if/elif chains for structural data
- Comprehensions are idiomatic Python — prefer them over manual loops for transforms
- Closures and factories create configured functions without classes
- Positional-only and keyword-only parameters make APIs clearer
__init__.py controls package exports — use __all__ for explicit public API- Lazy imports improve startup time for heavy dependencies