Back to blog
Backend Systemsbeginner

Python Functions & Type Hints: Write Code That Explains Itself

Master Python functions from basics to advanced patterns — type hints, *args/**kwargs, decorators, closures, comprehensions, and the functional tools used in real pipelines.

LearnixoMay 7, 20268 min read
Pythonfunctionstype hintsdecoratorsclosurescomprehensions
Share:š•

Functions Are the Core Unit of Python

Everything in Python is a function call or an object. Mastering how functions work — including how they're defined, called, composed, and typed — is the single highest-leverage skill in the language.


1. Function Basics

Python
def greet(name: str) -> str:
    return f"Hello, {name}!"

print(greet("Asma"))   # Hello, Asma!

Return values

Python
def divide(a: float, b: float) -> float | None:
    if b == 0:
        return None
    return a / b

result = divide(10, 2)   # 5.0
nothing = divide(10, 0)  # None

Always return a value — relying on implicit None from a missing return is a common source of bugs.

Multiple return values (tuple unpacking)

Python
def min_max(numbers: list[int]) -> tuple[int, int]:
    return min(numbers), max(numbers)

low, high = min_max([3, 1, 9, 2, 7])
print(low, high)   # 1 9

2. Type Hints

Type hints don't run at runtime — they're for your editor and for mypy. Write them on every function you define.

Built-in types

Python
def process(
    name: str,
    count: int,
    price: float,
    active: bool,
    tags: list[str],
    meta: dict[str, str],
) -> None:
    ...

Optional values

Python
from typing import Optional

def find_user(user_id: int) -> Optional[str]:   # old style
    ...

def find_user(user_id: int) -> str | None:       # Python 3.10+ (preferred)
    ...

Union types

Python
def parse_id(value: str | int) -> int:
    return int(value)

Literal types

Python
from typing import Literal

LogLevel = Literal["DEBUG", "INFO", "WARNING", "ERROR"]

def set_log_level(level: LogLevel) -> None:
    ...

TypeAlias and type (Python 3.12)

Python
from typing import TypeAlias

UserId: TypeAlias = int
Pipeline: TypeAlias = list[dict[str, str]]

# Python 3.12+
type UserId = int

Callable types

Python
from typing import Callable

def apply(fn: Callable[[int, int], int], a: int, b: int) -> int:
    return fn(a, b)

apply(lambda x, y: x + y, 3, 4)   # 7

3. Default Arguments and Keyword Arguments

Python
def create_report(
    title: str,
    rows: list[dict],
    format: str = "csv",
    include_header: bool = True,
) -> str:
    ...

# positional
create_report("Sales", data)

# keyword (any order, explicit)
create_report("Sales", data, include_header=False, format="json")

Never use mutable defaults — this is a classic Python trap:

Python
# WRONG — the list is shared across all calls
def add_item(item: str, items: list[str] = []) -> list[str]:
    items.append(item)
    return items

# CORRECT — use None as sentinel
def add_item(item: str, items: list[str] | None = None) -> list[str]:
    if items is None:
        items = []
    items.append(item)
    return items

4. *args and **kwargs

*args — variable positional arguments

Python
def sum_all(*numbers: int) -> int:
    return sum(numbers)

sum_all(1, 2, 3, 4, 5)   # 15

**kwargs — variable keyword arguments

Python
def build_query(**filters: str) -> str:
    parts = [f"{k}={v}" for k, v in filters.items()]
    return "&".join(parts)

build_query(status="active", role="admin")   # status=active&role=admin

Combining all parameter types

Python
def log(
    message: str,
    *tags: str,
    level: str = "INFO",
    **context: str,
) -> None:
    print(f"[{level}] {message} | tags={tags} | ctx={context}")

log("user created", "auth", "user", level="DEBUG", user_id="123")

Unpacking with * and **

Python
def add(a: int, b: int, c: int) -> int:
    return a + b + c

nums = [1, 2, 3]
add(*nums)   # 6

opts = {"b": 2, "c": 3}
add(1, **opts)   # 6

5. Closures

A closure is a function that remembers variables from the scope where it was defined, even after that scope has exited.

Python
def make_multiplier(factor: int) -> Callable[[int], int]:
    def multiply(x: int) -> int:
        return x * factor   # `factor` is captured from outer scope
    return multiply

double = make_multiplier(2)
triple = make_multiplier(3)

double(5)   # 10
triple(5)   # 15

Real use case: configurable pipeline steps

Python
def make_prefix_adder(prefix: str) -> Callable[[str], str]:
    def add_prefix(value: str) -> str:
        return f"{prefix}:{value}"
    return add_prefix

add_env = make_prefix_adder("prod")
add_env("database")   # "prod:database"

6. Decorators

A decorator is a function that wraps another function to add behaviour before, after, or around it.

Basic decorator

Python
from functools import wraps
from typing import Callable, Any

def log_call(fn: Callable) -> Callable:
    @wraps(fn)          # preserves the original function's metadata
    def wrapper(*args: Any, **kwargs: Any) -> Any:
        print(f"Calling {fn.__name__}")
        result = fn(*args, **kwargs)
        print(f"{fn.__name__} returned {result!r}")
        return result
    return wrapper

@log_call
def add(a: int, b: int) -> int:
    return a + b

add(2, 3)
# Calling add
# add returned 5

Decorator with arguments (decorator factory)

Python
def retry(times: int = 3) -> Callable:
    def decorator(fn: Callable) -> Callable:
        @wraps(fn)
        def wrapper(*args: Any, **kwargs: Any) -> Any:
            for attempt in range(times):
                try:
                    return fn(*args, **kwargs)
                except Exception as e:
                    if attempt == times - 1:
                        raise
                    print(f"Retry {attempt + 1}/{times} after error: {e}")
        return wrapper
    return decorator

@retry(times=3)
def fetch_data(url: str) -> dict:
    ...

Timing decorator (used in pipelines)

Python
import time

def timed(fn: Callable) -> Callable:
    @wraps(fn)
    def wrapper(*args: Any, **kwargs: Any) -> Any:
        start = time.perf_counter()
        result = fn(*args, **kwargs)
        elapsed = time.perf_counter() - start
        print(f"{fn.__name__} took {elapsed:.3f}s")
        return result
    return wrapper

7. Lambda Functions

Lambdas are anonymous single-expression functions. Use them for short transformations passed as arguments.

Python
# single expression only
double = lambda x: x * 2

# common use: as sort keys
users = [{"name": "Bob", "age": 30}, {"name": "Alice", "age": 25}]
users.sort(key=lambda u: u["age"])

# with map/filter
numbers = [1, 2, 3, 4, 5, 6]
evens = list(filter(lambda n: n % 2 == 0, numbers))   # [2, 4, 6]
doubled = list(map(lambda n: n * 2, numbers))          # [2, 4, 6, 8, 10, 12]

Prefer comprehensions over map/filter — they're more readable:

Python
evens = [n for n in numbers if n % 2 == 0]
doubled = [n * 2 for n in numbers]

8. Comprehensions

List comprehensions

Python
# [expression for item in iterable if condition]
squares = [x**2 for x in range(10)]
clean = [s.strip().lower() for s in raw_strings if s.strip()]

Dict comprehensions

Python
# {key_expr: value_expr for item in iterable}
word_lengths = {word: len(word) for word in ["hello", "world", "python"]}
inverted = {v: k for k, v in original_dict.items()}

Set comprehensions

Python
unique_domains = {email.split("@")[1] for email in emails}

Generator expressions (memory-efficient)

Python
# doesn't build the full list in memory
total = sum(x**2 for x in range(1_000_000))

# use when piping into another function
max_length = max(len(s) for s in strings)

9. functools Utilities

Python
from functools import reduce, partial, lru_cache

# lru_cache — memoize expensive pure functions
@lru_cache(maxsize=128)
def fibonacci(n: int) -> int:
    if n < 2:
        return n
    return fibonacci(n - 1) + fibonacci(n - 2)

# partial — pre-fill arguments
from functools import partial

def power(base: int, exp: int) -> int:
    return base ** exp

square = partial(power, exp=2)
cube = partial(power, exp=3)

square(5)   # 25
cube(3)     # 27

# reduce — fold a list into a single value
product = reduce(lambda acc, x: acc * x, [1, 2, 3, 4, 5])  # 120

10. Real-World Function Patterns for Pipelines

Step-based pipeline

Python
from typing import TypeVar

T = TypeVar("T")
Step = Callable[[T], T]

def run_pipeline(data: T, *steps: Step) -> T:
    result = data
    for step in steps:
        result = step(result)
    return result

# Usage
def normalize(text: str) -> str:
    return text.strip().lower()

def remove_punctuation(text: str) -> str:
    return "".join(c for c in text if c.isalnum() or c.isspace())

def tokenize(text: str) -> list[str]:
    return text.split()

clean_tokens = run_pipeline(
    "  Hello, World!  ",
    normalize,
    remove_punctuation,
    tokenize,
)
# ["hello", "world"]

Guard clauses (early return pattern)

Python
# AVOID deep nesting
def process_order(order: dict) -> str:
    if order:
        if order.get("status") == "pending":
            if order.get("items"):
                return "processing"

# PREFER guard clauses
def process_order(order: dict) -> str:
    if not order:
        return "no order"
    if order.get("status") != "pending":
        return "not pending"
    if not order.get("items"):
        return "no items"
    return "processing"

Exercises

Exercise 1: Write a clamp(value, min_val, max_val) function with full type hints that returns value restricted to [min_val, max_val].

Exercise 2: Write a decorator @validate_positive that raises ValueError if any positional argument is not a positive number.

Exercise 3: Build a compose(*fns) function that takes any number of single-argument functions and returns a new function that applies them left-to-right.

Exercise 4: Write a list comprehension that takes a list of file paths and returns only paths where the file extension is .py and the filename starts with test_.


Summary

| Pattern | Use When | |---------|---------| | Type hints | Always — on every function parameter and return | | Default None sentinel | When a mutable default (list, dict) is needed | | *args / **kwargs | Forwarding or collecting variable arguments | | Closures | Capturing config at construction time | | Decorators | Cross-cutting concerns: logging, retry, timing, auth | | Comprehensions | Transforming/filtering iterables (prefer over map/filter) | | lru_cache | Pure functions called repeatedly with the same args | | Guard clauses | Avoiding deeply nested conditionals |

Next: applying all of this inside classes and OOP patterns.

Enjoyed this article?

Explore the Backend Systems learning path for more.

Found this helpful?

Share:š•

Leave a comment

Have a question, correction, or just found this helpful? Leave a note below.