A function packages a chunk of logic, gives it a name, and lets you call it from anywhere. In a QA codebase, helpers like generate_email(), format_duration(), and build_api_url() save you from copy-pasting the same six lines into every test. Python's function syntax is the lightest you'll meet in any mainstream language: def name(params): body. No return type to declare, no class to wrap it in. This lesson covers def, parameters (positional, keyword, default, *args, **kwargs), return values, type hints, and docstrings.
The basic shape — def name(params):
def validate_response(status_code, body): if status_code != 200: return False if not body: return False return True
Read it line by line:
def — keyword that starts a function definition. Short for "define".
validate_response — the function name. snake_case by convention.
(status_code, body) — the parameters. Comma-separated, no types declared (yet).
: — colon to end the definition line. Same rule as if and for.
The body is indented. Four spaces, like every other Python block.
return value — sends the value back to the caller. Optional — a function with no return returns None.
Compared to Java's public static boolean validateResponse(int statusCode, String body) { … }, Python is positively bare. No public, no static, no return type. Less safety in some senses, more speed in others.
Return values — and what happens without one
A return statement sends a value back and exits the function immediately:
def http_status_label(code): if code < 400: return "ok" if code < 500: return "client-error" return "server-error"print(http_status_label(503)) # "server-error"
Multiple returns in one function are completely fine — often clearer than a single return at the bottom with nested ifs.
A function with no return (or return with no value) returns None:
This trips up beginners: result = print("hello") assigns None to result because print returns None. If you intend a function to be used for its return value, double-check it has a return.
Default parameter values
You can give a parameter a default — callers may then omit it:
This is closer to JavaScript's function f(x = 1) than Java's overloaded methods. It's cheap, ergonomic, and used everywhere in Python.
One trap. Don't use a mutable default like [] or {}. Python evaluates the default once when the function is defined, not on every call — so all calls share the same list. Use None and create a fresh value inside:
def add_test(name, tags=None): tags = tags or [] # fresh list every call return {"name": name, "tags": tags}
Keyword arguments — call by name
You don't have to pass arguments in order. Specify the parameter name and Python figures out where it goes:
Order is irrelevant when you use keyword arguments. They are especially useful for functions with many parameters — create_user(name="Alice", email="…", role="admin", verified=True) reads better than five positional values in a row.
You can mix positional and keyword arguments — but positional ones must come first:
If you don't know in advance how many arguments will be passed, prefix the parameter name with *. Inside the function it becomes a tuple:
def log(*messages): for msg in messages: print(f"[log] {msg}")log("started", "running test 1", "running test 2")log("completed in 3.2s")
messages is a tuple — ("started", "running test 1", "running test 2") in the first call. The name args is conventional but not required; *messages reads better when the meaning is "messages."
The mirror image when calling: * unpacks a sequence into positional arguments:
*args and **kwargs together let you forward arbitrary arguments through a wrapper function — a common pattern in test helpers and decorators (chapter 5).
Type hints — optional but loved
Type hints attach a declared type to each parameter and the return value. Python doesn't enforce them at runtime, but IDEs and mypy use them for autocompletion and static checks:
The shape is name: Type for parameters and -> Type after the ) for the return type. For complex types you'd import from typing:
from typing import Optionaldef find_user(user_id: int) -> Optional[dict]: """Return the user dict, or None if not found.""" ...
For QA helpers, hinting parameters and the return type is a small effort that pays back in IDE autocompletion. We won't insist on hints throughout this course — Python's idiom is "hint when it helps."
Docstrings — built-in documentation
A triple-quoted string immediately after the def line becomes the function's docstring. IDEs show it on hover; tools like Sphinx generate API docs from it.
def format_duration(milliseconds: int) -> str: """Convert milliseconds to a 'Xm Ys' label. Examples: format_duration(125_000) -> '2m 5s' format_duration(45_000) -> '0m 45s' """ minutes, seconds = divmod(milliseconds // 1000, 60) return f"{minutes}m {seconds}s"help(format_duration) # prints the docstring
Docstrings are the standard way to document Python code. Every function in a real test framework has one.
Triple-quoted docstring (optional but recommended)
Indented body — the actual logic
return — sends a value back. No return = returns None
Three QA helpers
A pocket library you might write in week one of a testing project:
from datetime import datetimedef generate_email(base: str = "qa") -> str: """Build a unique-looking email address for test data.""" stamp = datetime.now().strftime("%Y%m%d%H%M%S") return f"{base}+{stamp}@test.com"def format_duration(milliseconds: int) -> str: """Render a duration in 'Xm Ys' shape.""" minutes, seconds = divmod(milliseconds // 1000, 60) return f"{minutes}m {seconds}s"def build_api_url(env: str, path: str, **params) -> str: """Compose a URL with optional query parameters.""" base = f"https://{env}.api.example.com" if not params: return f"{base}{path}" query = "&".join(f"{k}={v}" for k, v in params.items()) return f"{base}{path}?{query}"print(generate_email()) # qa+20260506...@test.comprint(format_duration(125_000)) # 2m 5sprint(build_api_url("staging", "/users", role="admin")) # https://staging.api.example.com/users?role=admin
Three reusable helpers — under thirty lines total — that any test in your suite can call. That economy is the whole point of functions.
⚠️ Common mistakes
Mutable default arguments.def add(item, items=[]): looks fine but the same list is shared across every call without an explicit items=. Use items=None and create a new list inside the function.
Forgetting return. A function that does its work via side effects and returns nothing returns None. Calling result = my_func(...) and then trying to use result will fail with 'NoneType' object has no …. If you mean for the function to produce a value, write return value.
Calling without parentheses.format_duration (just the name) is the function object; format_duration(125_000) actually calls it. Reviewers see print(format_duration) regularly — fix is to add the ().
🎯 Practice task
Build a small QA helper module. 25-30 minutes.
Create qa_helpers.py.
Write def assert_status(actual: int, expected: int = 200) -> None: that prints "OK" if they match and raises AssertionError(f"expected {expected}, got {actual}") otherwise. Test with both a matching and a non-matching call (wrap the failing one in a try/except so the script still runs).
Write def build_user(name: str, email: str, role: str = "tester", **extras) -> dict: that returns a dict with name, email, role, plus everything from extras. Call it both with and without extras (e.g. verified=True, age=30). Print each result.
Write def format_duration(milliseconds: int) -> str: that returns a string like "2m 5s" for 125000. Use divmod.
Write def log(*messages: str) -> None: that prints each message prefixed with a timestamp (datetime.now().strftime(...)).
At the bottom of the file, exercise each function with at least two calls. Run with python qa_helpers.py and confirm all the outputs.
Stretch: add a def create_users(count: int, base: str = "qa") -> list: that uses generate_email() (also write that one) to produce a list of count user dicts via build_user. Confirm it returns a list of length count.
You can now define and call any function shape Python supports. The next lesson covers list comprehensions — Python's most loved single-line idiom for transforming and filtering data.
// tip to track lessons you complete and pick up where you left off across devices.