KAHIBARO
Discord Login Register

5.6 Functions and Type Hints

Why Functions Matter in Backend Python

In backend development you will write functions all the time. Functions let you:

As a backend developer you also want functions to be easy to understand and safe to use. That is where type hints help. Together, functions and type hints are one of your main tools for writing clear and reliable backend code.

In this chapter you will focus on:

Defining and Calling Functions

A function in Python is created with the def keyword.

python
def greet():
    print("Hello from the backend!")

You call (or invoke) the function by using its name followed by parentheses:

python
greet()   # prints: Hello from the backend!

This is a function with:

Parameters and Arguments

Most backend functions need input. You declare inputs as parameters. When you call the function, you pass arguments.

python
def greet_user(name):
    print(f"Hello, {name}!")
greet_user("Alice")   # "Alice" is the argument
greet_user("Bob")

Here:

You can define multiple parameters:

python
def create_user(username, email):
    print(f"Creating user {username} with email {email}")
create_user("alice", "alice@example.com")

Return Values

Functions can send a value back to the caller using return.

python
def add(a, b):
    result = a + b
    return result
sum_value = add(3, 5)     # sum_value is 8

If you do not use return, or you use return without a value, Python returns None:

python
def log_message(message):
    print(f"[LOG] {message}")
value = log_message("Server started")
print(value)   # prints: None

You can return any type:

python
def is_adult(age):
    return age >= 18              # bool
def get_default_port():
    return 8000                   # int
def get_default_headers():
    return {"Content-Type": "application/json"}   # dict

Default Parameter Values

You can give parameters default values. These are used when the caller does not pass a value.

python
def send_email(to_address, subject, body="", is_html=False):
    print(f"Sending email to {to_address}")
    print(f"Subject: {subject}")
    print(f"HTML? {is_html}")
    print(f"Body: {body}")

Examples of calling:

python
send_email("user@example.com", "Welcome")
send_email(
    "user@example.com",
    "Weekly report",
    body="Here is your report...",
)
send_email(
    "user@example.com",
    "Password reset",
    body="<p>Click the link</p>",
    is_html=True,
)

Order rule: parameters must follow this order:

  1. Required parameters (no default)
  2. Parameters with default values

Rule: In Python function definitions, all parameters without default values must come before any parameters with default values.
For example, def f(a, b=1) is valid, but def f(a=1, b) is an error.

Keyword Arguments

You can call functions using keyword arguments, where you specify parameter_name=value:

python
def connect_db(host, port, user, password):
    ...
connect_db(
    host="localhost",
    port=5432,
    user="app_user",
    password="secret",
)

You can change the order when you use keywords:

python
connect_db(
    password="secret",
    user="app_user",
    port=5432,
    host="localhost",
)

In backend code keyword arguments often make calls clearer, especially when many arguments are the same type, like several strings or numbers.

Variable-Length Arguments: *args and **kwargs

Sometimes you do not know how many arguments a function may receive. Python has two special patterns:

*args: Extra Positional Arguments

*args collects extra positional arguments into a tuple.

python
def log_all(*messages):
    for message in messages:
        print("[LOG]", message)
log_all("Server started")
log_all("DB connected", "Cache ready", "Worker started")

Here:

**kwargs: Extra Keyword Arguments

**kwargs collects extra keyword arguments into a dictionary.

python
def log_event(event_name, **data):
    print(f"Event: {event_name}")
    for key, value in data.items():
        print(f"  {key} = {value}")
log_event(
    "user_login",
    user_id=123,
    ip="192.168.0.1",
    country="US",
)

Here:

These patterns are very common in backend libraries, for example in logging, configuration, or database query helpers.

Return Early and Guard Clauses

Backend functions often need to validate input and stop early if something is wrong.

python
def normalize_page(page):
    if page < 1:
        return 1
    return page

This is called a guard clause. It keeps functions short and clear.

Another example:

python
def get_first_or_none(items):
    if not items:
        return None
    return items[0]

Why Type Hints Matter for Backend Code

Python is dynamically typed, so the interpreter does not require you to specify types. You can write:

python
def add(a, b):
    return a + b

This works, but:

Type hints solve this. They are optional hints that describe the expected types.

Type hints are checked by tools such as mypy or pyright, not by Python at runtime.

Basic Type Hints for Function Parameters

You add type hints after a colon :.

python
def add(a: int, b: int):
    return a + b

This means:

Python still allows other types at runtime, but type checkers and IDEs will warn you.

Some basic built-in types you will use:

TypeExample valueWhere used in backend
int200, 404, 8080Status codes, ports, IDs, counts
float3.14, 0.5Prices, percentages, time intervals
str"GET", "application/json"HTTP methods, headers, messages
boolTrue, FalseFlags like is_active, is_admin
bytesb"raw data"Binary files, network data

Example with parameters:

python
def send_response(status_code: int, body: str) -> None:
    print(f"Status: {status_code}")
    print("Body:", body)

Type Hints for Return Values

You annotate the return type with -> after the parameter list.

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

If a function returns nothing useful, use -> None:

python
def log_error(message: str) -> None:
    print(f"[ERROR] {message}")

If a function returns different types in different branches, you should try to make it consistent. If that is not possible, you will use unions, which you will see later.

Rule: Every non trivial function in backend code should have an explicit return type hint.
For example, use -> int, -> str, -> dict, or -> None, instead of leaving it unannotated.

Using Type Hints with Default Values

Default values work together with type hints:

python
def paginate(page: int = 1, page_size: int = 20) -> None:
    print(f"Page {page}, size {page_size}")

You still specify the type, even if a default value is present. The default must match the type.

Typing Common Data Structures

Backend code uses collections all the time. You will use the typing module.

In Python 3.9 or newer, you can write:

python
from typing import Dict, List, Tuple, Set

or use built-in generics like list[int]. Both styles are common. Here you will show both, then focus on the newer style.

Lists

A list of integers:

python
from typing import List
def sum_scores(scores: List[int]) -> int:
    return sum(scores)

Newer syntax:

python
def sum_scores(scores: list[int]) -> int:
    return sum(scores)

Dictionaries

A dictionary from str to int, for example a mapping of status code names to numbers:

python
from typing import Dict
def get_status_codes() -> Dict[str, int]:
    return {"ok": 200, "not_found": 404}

Newer syntax:

python
def get_status_codes() -> dict[str, int]:
    return {"ok": 200, "not_found": 404}

Tuples

A tuple with a fixed shape such as (host, port):

python
from typing import Tuple
def get_default_db_address() -> Tuple[str, int]:
    return ("localhost", 5432)

Newer syntax:

python
def get_default_db_address() -> tuple[str, int]:
    return ("localhost", 5432)

Optional and None

Many backend functions can return "no result" instead of a value. Type hints should represent that.

You use Optional[T] or T | None.

python
from typing import Optional
def find_user_email(user_id: int) -> Optional[str]:
    # None means not found
    if user_id == 1:
        return "admin@example.com"
    return None

In newer syntax:

python
def find_user_email(user_id: int) -> str | None:
    if user_id == 1:
        return "admin@example.com"
    return None

To annotate a parameter that can be None:

python
def send_welcome_email(email: str | None) -> None:
    if email is None:
        print("No email to send welcome message to.")
        return
    print(f"Sending welcome email to {email}")

Unions: One of Several Types

Sometimes a function can accept or return one of several types.

You can write Union[A, B] or A | B.

python
from typing import Union
def parse_user_id(raw: Union[int, str]) -> int:
    if isinstance(raw, int):
        return raw
    return int(raw)

Newer syntax:

python
def parse_user_id(raw: int | str) -> int:
    if isinstance(raw, int):
        return raw
    return int(raw)

Typing *args and **kwargs

You can give types to variable-length arguments too.

Typed *args

python
from typing import Any
def log_many(*messages: str) -> None:
    for msg in messages:
        print("[LOG]", msg)

This says: every positional argument must be a str.

Typed **kwargs

python
def build_filters(**filters: str) -> dict[str, str]:
    # filters might be: status="active", role="admin"
    return filters

This says:

Callable Types

Sometimes you pass one function into another, for example a callback.

You can use Callable to type this.

python
from typing import Callable
def run_query_with_logger(query: str, logger: Callable[[str], None]) -> None:
    logger(f"Running query: {query}")
    # execute query here
    logger("Query finished")

You can use it like this:

python
def simple_logger(message: str) -> None:
    print("[DB]", message)
run_query_with_logger("SELECT 1", simple_logger)

Type Aliases for Better Readability

If a type is long or used many times, you can give it a name.

python
from typing import Dict, Any
JSON = Dict[str, Any]
def parse_request_body(body: bytes) -> JSON:
    # pretend to parse JSON
    return {"raw": body.decode("utf-8")}

Newer syntax:

python
from typing import Any
JSON = dict[str, Any]
def parse_request_body(body: bytes) -> JSON:
    return {"raw": body.decode("utf-8")}

This makes function signatures easier to read, especially in backend code where you often deal with JSON-like structures.

Practical Backend Examples with Type Hints

Here are some realistic small backend-style functions with type hints.

Parsing Query Parameters

python
def parse_int_param(raw: str | None, default: int = 0) -> int:
    if raw is None:
        return default
    try:
        return int(raw)
    except ValueError:
        return default

Usage:

python
page_str = "5"          # pretend from query string
page = parse_int_param(page_str, default=1)   # page is int

Building an HTTP Response Object

python
from typing import Any
Headers = dict[str, str]
Body = dict[str, Any]
def make_json_response(
    data: Body,
    status_code: int = 200,
    headers: Headers | None = None,
) -> dict[str, Any]:
    if headers is None:
        headers = {}
    headers.setdefault("Content-Type", "application/json")
    return {
        "status_code": status_code,
        "headers": headers,
        "body": data,
    }

Finding a User

python
from typing import TypedDict, Optional
class User(TypedDict):
    id: int
    username: str
    email: str
def get_user_by_id(user_id: int) -> Optional[User]:
    if user_id == 1:
        return {
            "id": 1,
            "username": "admin",
            "email": "admin@example.com",
        }
    return None

Here User is a typed dictionary. You will learn more about structured models later, but this gives you a feel for how type hints describe real backend data.

Type Checking and Tooling

Type hints alone do nothing at runtime. To benefit from them you use tools:

A typical workflow:

  1. Add type hints to your functions
  2. Run mypy in your project
  3. Fix reported issues
  4. Repeat

You do not need to be perfect. Even partial type coverage is very useful in real backend projects.

Common Pitfalls with Functions and Type Hints

Mutable Default Arguments

Never use a mutable object as a default value.

Bad:

python
def add_header(headers: dict[str, str] = {}):   # bad
    headers["X-App"] = "my-app"
    return headers

This dictionary is shared between calls.

Correct:

python
from typing import Optional
def add_header(headers: Optional[dict[str, str]] = None) -> dict[str, str]:
    if headers is None:
        headers = {}
    headers["X-App"] = "my-app"
    return headers

Type hints help you see that headers can be None.

Inconsistent Return Types

Avoid functions that sometimes return one type and sometimes another unrelated type.

Bad:

python
def get_user_maybe(user_id: int):
    if user_id == 1:
        return {"id": 1}
    return False

Better:

python
from typing import Optional
def get_user_maybe(user_id: int) -> Optional[dict[str, int]]:
    if user_id == 1:
        return {"id": 1}
    return None

Summary

In this chapter you learned:

You now have the tools to write clear, typed functions in Python, which is the foundation for building reliable backend services.

Views: 8

Comments

Please login to add a comment.

Don't have an account? Register now!