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}