Podcast Script: Functions
Host: Welcome back to Python Essentials. This is Chapter 5 of 13, and today we're tackling functions — the single most important structural unit in Python. If you're building AI systems, data pipelines, or any production service your team is shipping, functions are the fundamental building block that makes code reusable, testable, and safe to maintain. Your organization invested in this training to deepen your engineering capability, and this is where that investment starts to pay real dividends — because the moment you can design clean function contracts, you can contribute code your teammates trust enough to call without reading. In the last chapter we covered control flow — the if-branches, the for and while loops, the logic that decides what happens when. Now the question is: how do you take those blocks of logic and wrap them up into named, reusable units that the rest of your codebase can call? Picture this: it's 3 AM, an incident fires, and you're tracing a bug through a data pipeline. Whether that's painful or manageable depends almost entirely on how the functions in that pipeline were designed. You'll practice all of this across five hands-on exercises, but first, let's build the mental model. We'll explore four big ideas — defining functions, parameters and return values, docstrings, and scope — and tie them together at the end.
Expert: Great framing. Let me start with what a function actually is, because this is where a lot of engineers coming from other languages get tripped up. A function in Python is a named block of code that performs a specific task. You define it once, and then you can call it — meaning execute it — from anywhere else in your program. The keyword that creates a function is def, spelled D-E-F, short for "define." When you write def followed by a name, some parentheses, a colon, and then an indented body, you're telling Python: here is a reusable piece of logic, remember it under this name. Now here's the subtle part that matters in production. When Python sees a def statement, it doesn't run the code inside. It does three things. First, it compiles the body into an internal form. Second, it wraps that up into a function object — yes, an object, because in Python functions are first-class citizens, meaning you can pass them around like any other value. Third, it binds that object to the name you chose, in whatever namespace you're currently in. The body itself only runs later, when somebody actually calls the function by writing its name followed by parentheses. Why does this matter? Because it means two functions can refer to each other mutually, even before both are defined — Python only resolves the names at call time, not at definition time. It also explains one of the most famous gotchas in the language, which I'll come back to later, around default parameter values being evaluated once, at definition, not every time the function runs. Now, the anatomy of every function definition is the same: the def keyword, a descriptive name — by convention lowercase with underscores between words, like calculate underscore total — parentheses that hold any parameters, a colon, and then an indented body. Inside that body you can have any Python code at all: assignments, loops, conditionals, calls to other functions, and typically a return statement. If no return statement is hit, the function implicitly hands back a special value called None, which is Python's way of saying "nothing meaningful here." That implicit None is deliberate — it means every single function call is guaranteed to produce some result, even if that result is just "none." One last production detail: treat each function as one unit of responsibility. If you find yourself naming a function validate-and-save-and-notify, that's three functions trying to live in one body. Break them up. Small, focused, well-named functions are the foundation of code your team can actually maintain.
Host: So def creates a named, callable unit, and each function should do one thing well. That sets up the next question naturally — how do we actually get data into a function and get a result back out?
Expert: Right, and this is where the real design work happens. A parameter is a variable listed inside the parentheses of a function definition. Think of it as a labeled slot that the function promises to accept. An argument is the actual value you hand in when you call the function. So the function says "I need a name and a price" — those are parameters. When you later call it with the string "Alice" and the number forty-nine ninety-nine, those values are the arguments. Python binds the arguments to the parameter names in a new, isolated workspace just for this call. Now, Python gives you a richer parameter system than most languages, and you need to know the tools. First, positional parameters. These are required — if the function asks for them, you must supply them, in order. Second, keyword parameters with default values. You write the parameter name, an equals sign, and a default. If the caller omits it, the default kicks in. This is how you make configuration optional without forcing every caller to supply every knob. The rule is: required positional parameters come first, then optional ones with defaults. You can also pass arguments by name at the call site — writing the parameter name, equals, value — which makes your calling code self-documenting. Instead of passing the number eight thousand eighty into some function and leaving the reader to guess what that means, you write "port equals eight thousand eighty" and suddenly it's obvious. Production code strongly favors keyword arguments for optional values, because they survive refactoring — if someone reorders the parameters, your call still works. Now, on the output side: the return statement. When the function reaches a return, it stops executing, and the value after the word "return" gets handed back to whoever called it. That caller can assign it to a variable, pass it to another function, or use it in an expression. If there's no return, you get None back, as I mentioned. You can also return multiple values — Python will automatically bundle them into a tuple, which is an ordered, immutable group, and the caller can unpack them into separate names. For more than two or three values, it's much better to return a dictionary with descriptive keys, because then your calling code reads like prose instead of cryptic index lookups. Now here's the trap every Python engineer has to learn once. Never use a mutable object — a list, a dictionary, a set — as a default parameter value. Why? Because defaults are evaluated once, at the moment the function is defined, not each time it's called. So if you write "log equals empty list" as a default, every call that relies on that default shares the same list. The first call appends a message, the second call sees that message still sitting there, and suddenly your function is leaking state between unrelated requests. In a long-running web service, one user's data can literally show up in another user's response. The fix is the sentinel pattern: set the default to None, and then inside the function body, check if the parameter is None and create a fresh list right there. This guarantees each call gets its own independent object. It's such a common pitfall that linting tools flag it automatically.
Host: That mutable default trap is exactly the kind of thing that silently ships to production and shows up weeks later as a mysterious data leak. Okay — so we know how to shape inputs and outputs. But a function is only useful if your teammates can actually figure out how to call it without reading the source. That brings us to documentation.
Expert: Exactly, and this is where docstrings come in. A docstring is a string literal — a piece of text in triple quotes — that you place as the very first statement inside a function's body. It has to be first. If you put any other statement before it, Python stops treating it as documentation. What makes a docstring different from a comment? Comments get stripped away after Python parses the file. Nothing can retrieve them at runtime. A docstring, by contrast, is stored on the function as something called the double-underscore-doc attribute — meaning it lives in memory alongside the function object itself. Any tool that has a reference to the function can read its docstring. That runtime accessibility is the whole point. Three main consumers care about this. First, Python's built-in help function — type help and the function name in the interactive Python shell, and it prints the docstring. Second, your IDE — when you hover over a function call in an editor like VS Code or PyCharm, the tooltip that pops up is reading the docstring. Third, documentation generators like Sphinx and pdoc, which scan your code and produce HTML reference sites for your team or your users. So a docstring isn't just a note — it's structured data that your whole tooling ecosystem consumes. Now, Python's official style guide, a document called PEP 257, defines the minimum: use triple double quotes, start with a one-line summary in the imperative mood — write "Convert a duration to seconds," not "Converts a duration to seconds" — and leave a blank line before the closing quotes on multi-line docstrings. But PEP 257 doesn't standardize how to document parameters and return values, so the community has three conventions: Google style, NumPy style, and reStructuredText style. This course uses Google style because it's the most readable in plain text and it's supported by all the major tooling. A complete Google-style docstring has four pieces. A one-line summary. An optional extended description. An Args section, spelled A-R-G-S, listing each parameter with what it is and what values are acceptable. A Returns section describing the type and meaning of the return value. And a Raises section listing the exceptions the function can throw, so the caller knows what to handle. Here's the production mindset: treat the docstring as a binding contract. If it says the function raises a value error on empty input, write a test that feeds empty input and asserts that error happens. If it says the function returns an integer, add a type hint that says so and run a static checker. When the implementation changes, the docstring changes in the same commit. A docstring that lies is worse than no docstring at all, because it gives callers false confidence. Companies like Google and Stripe put "does the docstring match the implementation?" right in their code review checklists, because stale documentation costs more engineering time than missing documentation does.
Host: So a docstring isn't decoration — it's a contract that your tooling and your teammates both depend on. Last major topic: scope. This is the one that produces the weirdest, most frustrating bugs. What do we need to know?
Expert: Scope is about visibility — which variables can be seen from which parts of your code. Every time you call a function, Python creates a brand new local workspace, technically called a stack frame, that holds the function's parameters and any variables you assign inside the body. That workspace is isolated. When the function returns, the workspace is destroyed, and all those local variables disappear. This isolation is exactly what makes functions reusable — two calls to the same function can't corrupt each other, because each one has its own private namespace. Now, Python resolves variable names using a rule called LEGB — spelled L-E-G-B. That stands for Local, Enclosing, Global, Built-in, and Python searches those four scopes in that exact order. Local means inside the current function. Enclosing means inside an outer function, if you've nested one function inside another. Global means the module level — variables defined at the top of the file, outside any function. Built-in means names Python itself provides, like the print function, the length function, and the True and False values. Python walks through those four in order, uses the first match, and raises a name error if none of them have the name. Here's the rule that bites everyone at least once. If a name appears on the left side of an assignment anywhere in a function body — even on the last line — Python marks that name as local for the entire function, from the first line onward. So if you try to read a variable called counter on line three, and then reassign it on line five, Python treats line three as a read of a local variable that hasn't been assigned yet, and throws an error called unbound local error. The global variable named counter, sitting at module level, is invisible at that point, because Python has already decided this is a local name. The fix, when you really do want to modify a module-level variable, is the global keyword. You write "global" followed by the variable name at the top of your function body, and that tells Python "every reference to this name in this function points at the module level, not at a local variable." But — and this is critical for production — just because you can use the global keyword doesn't mean you should. Functions that modify global state are a design smell. Their behavior depends on how many times other code has called them before. Unit tests become order-dependent. Concurrent execution breaks, because two threads can read the same global value and both try to increment it, losing updates. The far better pattern is pure functions — functions whose output depends only on their inputs, with no hidden state. Pass the data in through parameters, return the updated data back out, and let the caller decide where to store it. Pure functions scale horizontally without any modification, they're trivially testable, and they never surprise you at 3 AM. When you genuinely need the global keyword, it's a sign that your design needs reshaping. There's a cousin called nonlocal that does the same thing for the enclosing scope of a nested function, but use it just as sparingly.
Host: That's a lot of depth. Before we preview the labs, give us the two or three production lessons that matter most — if someone remembers nothing else from this chapter, what should stick?
Expert: Three things. First: never use a mutable object as a default parameter value. No default lists, no default dictionaries, no default sets. Use None as the sentinel, and create the fresh object inside the function body. This one rule will save your team from entire categories of state-leak bugs in production services. Second: always raise a specific, informative exception for error cases instead of returning None to signal failure. If your function accepts bad input, say so loudly with a value error or a type error that names the problem. A silent None return forces every caller to check the result and guess why it failed, and it hides problems that should be triggering alerts in your monitoring system. On that note — never catch broad exceptions and swallow them inside a utility function. Catch only what you can meaningfully handle, and let everything else propagate so the boundary layer of your application can log it and alert on it. Third: keep functions focused on a single responsibility, and document them as if a new teammate will be reading the docstring at two in the morning during an incident. If your function has more than five parameters, that's a design smell — decompose it or group related parameters into a data object. If your function has no docstring, your on-call engineer is going to have to read the implementation during an outage to figure out what it does, and that costs real money. One more worth calling out: don't shadow built-in names. Never name a parameter "list" or "dict" or "type" or "input" — you'll replace the built-in inside your function and get confusing errors when you try to use it.
Host: Those three lessons alone are worth bringing back into your team's next code review. Now let's talk about how you'll practice this.
Expert: In the exercises, you'll work through five objectives that map directly to what we covered. The first lab has you defining and calling simple functions, getting comfortable with the def keyword and the distinction between defining something and actually executing it. The second lab focuses on parameters — positional, keyword, and defaults — so you can feel how Python binds arguments at call time. The third lab is about return values: sending single results, returning multiple values via tuples, and returning structured dictionaries for richer output. The fourth lab is all about docstrings — writing Google-style documentation that tools can consume. And the fifth lab dives into scope, including the unbound local error and the safe way to work with module-level state. Each exercise has its own audio overview that goes deeper into the specific patterns you'll be building.
Host: Let's wrap it up. After this chapter you now understand three big things. You understand how the def keyword creates a reusable, first-class function object, and why the anatomy of a function — signature, docstring, body, return — is the same pattern every production codebase relies on. You understand how parameters and return values form the input-output contract of a function, why default values should never be mutable objects, and why keyword arguments at the call site make your code self-documenting. And you understand Python's LEGB scope resolution, the unbound local error, and why pure functions that avoid global state are safer, more testable, and more scalable than functions that reach for shared state. With this depth, you now have the vocabulary to contribute to your team's architecture discussions about service design, code review standards, and testing strategy — not just to consume the codebase but to shape it. The chapter quiz will focus on how Python handles default parameters, which docstring convention to use and why, Python's scope resolution rules, and the subtleties of Python's argument binding. Pay close attention to the mutable default trap and the unbound local error — those are the two trickiest decision points. In the next chapter, Modules and Imports, we'll take the functions you've mastered here and organize them into reusable files and packages that scale across large codebases. That's where your functions stop being script-level helpers and start becoming proper building blocks in your team's shared infrastructure. See you there.
Want to go deeper? Explore disciplines with hands-on labs, quizzes, and chapter podcasts.