Python Functions
Function definitions, arguments, closures, and lambda expressions.
Function Signatures
Function with type hints and default — default must be immutable, annotations are documentation
def get_status(host: str, port: int = 443) -> bool:
return check_connection(host, port)
Keyword-only args after * — caller must name them: fetch(url, timeout=10), prevents positional errors
def fetch(url: str, *, timeout: int = 30, verify: bool = True):
...
Positional-only before / — Python 3.8+, msg cannot be passed as keyword argument
def log(msg: str, /, level: str = "INFO"):
print(f"[{level}] {msg}")
Variable Arguments
*args — collects positional arguments into a tuple, use for variable-length input
def merge(*dicts: dict) -> dict:
result = {}
for d in dicts:
result.update(d)
return result
**kwargs — collects keyword arguments into a dict, use for flexible configuration
def connect(**options: str) -> None:
host = options.get("host", "localhost")
port = int(options.get("port", 443))
Mixed args — positional first, then *args, then keyword-only, then **kwargs
def configure(host, port, *args, **kwargs):
# host, port are required positional
# args catches extra positional
# kwargs catches extra keyword
...
Return Values
Multiple return values — actually returns a tuple, caller unpacks: h, i, p = func()
def parse_endpoint(endpoint: str):
host, port = endpoint.rsplit(":", 1)
return host, int(port)
host, port = parse_endpoint("10.50.1.20:443")
Return type union — Python 3.10+, function may return str or None
def process(data: bytes) -> str | None:
if not data:
return None
return data.decode("utf-8")
Lambdas and Sorting
Lambda — anonymous single-expression function, use for sorted(key=…), map(), filter()
sorted(devices, key=lambda d: d["hostname"])
Lambda with tuple key — multi-level sort, severity first then timestamp within same severity
sorted(alerts, key=lambda a: (a["severity"], a["timestamp"]))
Functional Tools
Partial application — freeze some arguments, create specialized function from general one
from functools import partial
fetch_json = partial(fetch, headers={"Accept": "application/json"})
# Now fetch_json(url) always sends JSON accept header
Callable type hint — specifies function signature: takes str, returns bool
from typing import Callable
def apply(func: Callable[[str], bool], items: list[str]) -> list[str]:
return [item for item in items if func(item)]
Walrus in comprehension — call function once, test and use result, avoids double evaluation
results = [y for x in items if (y := transform(x)) is not None]
Generators
Generator function — yield produces values lazily, caller iterates without loading all into memory
def gen_ips(subnet: str):
"""Yield all host IPs in a /24 subnet."""
base = subnet.rsplit(".", 1)[0]
for host in range(1, 255):
yield f"{base}.{host}"
for ip in gen_ips("10.50.1.0"):
if ping(ip):
print(f"{ip} is alive")
yield from — delegates to sub-generator or iterable, flattens nested generation
def read_logs(paths: list[str]):
for path in paths:
yield from open(path)
Decorators (Basic)
Decorator with arguments — three levels of nesting: factory, decorator, wrapper
def retry(max_attempts=3):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(max_attempts):
try:
return func(*args, **kwargs)
except Exception:
if attempt == max_attempts - 1:
raise
return wrapper
return decorator
Preserve function metadata — without @wraps, func.name and func.doc are lost
from functools import wraps
def log_call(func):
@wraps(func)
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__}")
return func(*args, **kwargs)
return wrapper
Memoization decorator — caches return values by arguments, maxsize=None for unbounded
from functools import lru_cache
@lru_cache(maxsize=128)
def dns_lookup(hostname: str) -> str:
return socket.gethostbyname(hostname)
Advanced Patterns
Guard assertions — fail fast with clear message, disabled with python -O in production
def validate(data: dict) -> dict:
assert "host" in data, "missing host"
assert "port" in data, "missing port"
return data
Generator-based context manager — yield separates setup and teardown, cleaner than class-based
from contextlib import contextmanager
@contextmanager
def timer(label: str):
start = time.time()
yield
elapsed = time.time() - start
print(f"{label}: {elapsed:.2f}s")
Callable class — instances become callable with obj(), use for stateful functions
class RateLimiter:
def __init__(self, max_calls: int):
self.max_calls = max_calls
self.calls = 0
def __call__(self, func):
@wraps(func)
def wrapper(*args, **kwargs):
if self.calls >= self.max_calls:
raise RuntimeError("Rate limit exceeded")
self.calls += 1
return func(*args, **kwargs)
return wrapper
Closure with nonlocal — inner function captures outer variable, nonlocal allows mutation
def make_counter():
count = 0
def increment():
nonlocal count
count += 1
return count
return increment
Async function — returns coroutine, must be awaited: result = await fetch(url)
async def fetch(url: str) -> dict:
async with aiohttp.ClientSession() as session:
async with session.get(url) as resp:
return await resp.json()
Generic type hints — Python 3.9+ allows list[str] instead of List[str] from typing
def handle(items: list[str]) -> dict[str, int]:
return {item: len(item) for item in items}