Python Functions โ The def Keyword Explained Properly
Everything you actually need to know about Python functions โ def, parameters vs arguments, return values, default arguments, *args and **kwargs, scope, lambda, recursion, and the exact TypeError you'll hit while learning this.
Last Updated
March 2026
Read Time
20 min
Level
Beginner
What is a Function in Python?
A function is a named, reusable block of code that does one job. You write the logic once, give it a name, and call that name whenever you need the job done again โ instead of copy-pasting the same five lines into twelve different places in your script. That's the entire pitch, and it's the single biggest jump in code quality most beginners make once it actually clicks.
In Python, you define a function using the def keyword, give it a name, list whatever inputs it needs in parentheses, and indent the block of code that runs when it's called. Something like def calculate_discount(price, percent): followed by the logic underneath. Call it later with calculate_discount(500, 10) and Python runs that block with those specific values plugged in.
Here's the thing nobody tells you early enough: functions aren't really about avoiding repetition, even though that's how they're usually introduced. The real value shows up once your project grows past 200 lines and you can't hold the whole thing in your head anymore. A well-named function โ send_confirmation_email(user) instead of forty lines of SMTP logic sitting inline โ lets you read the calling code and understand what's happening without caring how it happens. That's the actual skill you're building here, not just syntax memorisation.
I once inherited a script where someone had written the same 15-line 'validate and format phone number' logic six separate times across the file, each slightly different because bugs got fixed in one copy and never propagated to the others. Two of the six copies had actual different behaviour by the time I found it โ not intentionally, just drift. One function, called six times, would have made that impossible. This guide covers everything from a bare-minimum function definition to *args, **kwargs, scope rules, lambda functions, recursion, and the specific errors you'll hit along the way.
History โ How Python's Function Syntax Evolved
Functions have been core to Python since its very first release โ this isn't a bolted-on feature, it's foundational. But the syntax around them has picked up real capability over the decades, and knowing the timeline helps when you're reading code from different eras.
- โถ
1991 โ Python 0.9.0 โ def, return, and basic positional parameters were present from Python's earliest public release. Functions were first-class citizens from day one โ you could already pass them around as values, store them in variables, and pass them as arguments to other functions.
- โถ
1994 โ Python 1.0 โ Added lambda, along with map, filter, and reduce โ bringing genuine functional-programming tools into the language, allowing small anonymous functions without a full def block.
- โถ
2001 โ Python 2.1 โ Introduced nested scopes properly, allowing inner functions to read variables from their enclosing function's scope (closures), which unlocked decorator patterns years before decorators had dedicated syntax.
- โถ
2004 โ Python 2.4 (PEP 318) โ Introduced the @decorator syntax, letting one function wrap and modify another using a clean @ prefix rather than the old-style manual function = decorator(function) reassignment pattern.
- โถ
2006 โ Python 2.5 (PEP 342) โ Enhanced generators with the ability to send values back into a generator function via .send(), laying groundwork for what eventually became async/await syntax.
- โถ
2008 โ Python 3.0 โ Added keyword-only arguments (parameters after a bare * in the signature) and function annotations (type hints on parameters and return values), both aimed at making function signatures more explicit and self-documenting.
- โถ
2015 โ Python 3.5 (PEP 492) โ Introduced async def and await, extending function syntax to cover asynchronous coroutines as a distinct, first-class kind of function.
- โถ
2018 โ Python 3.8 (PEP 570) โ Added positional-only parameters using a / in the signature, giving developers explicit control over whether an argument can be passed by keyword at all โ useful for library authors who want to rename a parameter later without breaking callers.
- โถ
2026 โ Current state โ Function syntax is now genuinely rich: positional-only params, keyword-only params, defaults, *args, **kwargs, type hints, decorators, and async support all coexist in one coherent grammar. Most beginner code only ever touches a fraction of this โ and that's fine, the basics haven't changed at all since 1991.
Key Characteristics of Python Functions
Python functions are more flexible than most beginners realise on day one. Here are the 12 characteristics that actually matter once you start writing real code:
Every function starts with def function_name(parameters):, followed by an indented block. The name should describe what the function does โ get_user_age(), not do_stuff().
Functions in Python are values, exactly like integers or strings. You can assign them to variables, store them in lists, and pass them as arguments to other functions โ this is what makes decorators and callbacks possible.
A function without an explicit return automatically returns None. This trips up beginners who forget the keyword and then wonder why result comes back as None instead of the value they computed.
Positional, keyword, default-valued, *args (variable positional), and **kwargs (variable keyword) parameters can all combine in a single signature, in a specific required order.
Variables created inside a function exist only inside that function unless explicitly declared global. This isolation is what prevents functions from silently stepping on each other's variables.
A string literal right after the def line โ triple-quoted by convention โ becomes the function's docstring, accessible via help(function_name) or function_name.__doc__.
A function can call itself from within its own body. This is how recursive algorithms like factorial calculation or tree traversal get written in Python.
lambda x: x * 2 creates a small, unnamed function in a single expression โ useful for short throwaway logic passed to sort(), map(), or filter(), but limited to a single expression, no statements.
def greet(name: str) -> str: annotates expected parameter and return types. Python doesn't enforce these at runtime โ they're purely documentation for humans and tools like mypy.
Adding async before def creates a coroutine function, meant to be awaited inside an event loop rather than called directly โ the foundation of Python's asyncio concurrency model.
Functions can be defined inside other functions, and the inner function can 'remember' variables from the outer function's scope even after the outer function has finished running.
Unlike Java or C++, Python doesn't support multiple functions with the same name but different parameter types. Defining a name twice simply replaces the earlier definition entirely.
How a Function Call Executes โ Flowchart
Calling a function isn't just 'jumping to some code.' Python does a specific sequence of steps every single time โ binding arguments to parameters, creating a fresh local scope, running the body, and handing back whatever the return statement produces (or None if there isn't one). Here's that pipeline laid out precisely.
Code Execution Flow โ from source to output
Key insight: step 3 โ the fresh local scope โ is exactly why a variable named price inside calculate_discount() has zero relationship with a variable named price sitting in your main script. Each call gets its own clean namespace, used once, then discarded. This is also why recursive functions don't confuse their own local variables between different levels of recursion โ every call, no matter how deep, gets a brand new scope.
How Functions Actually Work โ Parameters, Arguments, and Return Values
Three concepts sit at the core of every function you'll write: the distinction between parameters and arguments (people use these words interchangeably in casual conversation, but they mean different things), how return actually works, and how default arguments quietly become one of Python's most infamous gotchas.
๐ท๏ธ Parameters vs Arguments โ The Distinction That Actually Matters
A parameter is the name listed in the function definition: def greet(name): โ name is the parameter. An argument is the actual value you pass when calling it: greet("Rohan") โ "Rohan" is the argument. Interviewers ask this distinction specifically because it reveals whether someone actually understands function mechanics or has just been pattern-matching syntax. It's a small distinction, but it's precise, and precision here pays off once you start reading error messages that use these exact terms.
โฉ๏ธ return โ What It Actually Does
return does two things simultaneously: it immediately stops the function's execution โ any code after it in that function never runs โ and it hands a value back to whoever called the function. Forgetting return is one of the most common beginner mistakes: writing def add(a, b): print(a + b) instead of return a + b means the function prints the sum but gives back None to anything trying to use the result. Try result = add(3, 4) followed by result + 10 and you'll get TypeError: unsupported operand type(s) for +: 'NoneType' and 'int' โ a genuinely confusing error the first time you see it, because the function looked like it worked fine when you ran it directly.
โ ๏ธ The Mutable Default Argument Trap
This is Python's single most famous function-related gotcha, and it catches experienced developers too, not just beginners. Write def add_item(item, basket=[]): and the empty list [] is created exactly once โ when the function is defined, not each time it's called. Every call that relies on the default shares the exact same list object. Call add_item("apple") then add_item("banana") with no second argument each time, and you'd expect two separate baskets โ instead you get one basket containing both items, because they were both silently appending to the same shared list. The fix is the standard idiom: def add_item(item, basket=None): if basket is None: basket = []. This pattern shows up constantly in real code reviews.
๐ Scope โ Local, Enclosing, Global, Built-in (LEGB)
Python resolves variable names using what's called the LEGB rule: it checks Local scope first (inside the current function), then Enclosing scope (any outer function it's nested inside), then Global scope (module-level), then Built-in scope (Python's own reserved names like len or print). A variable assigned inside a function is local by default โ trying to modify a module-level variable from inside a function without the global keyword just creates a new local variable instead, which is a common source of confusion when a function seems to 'not update' a global value.
Simple rule to remember: parameters are names in the definition, arguments are values at the call site, return hands back a value and stops execution immediately, and default arguments should never be mutable objects like lists or dicts unless you genuinely want that sharing behaviour (which is rare).
Positional vs Keyword vs Default vs *args vs **kwargs โ Key Differences
A single function signature can combine all five of these parameter styles, in a specific required order. This table lays out what each one actually does and when to reach for it.
Defining Functions โ Python vs Other Languages
Function syntax is one of the sharpest contrasts between Python and statically typed languages, and it explains a lot about why Python feels faster to prototype in but riskier to scale without discipline.
My honestly opinionated take: Python's decision to skip function overloading entirely is the right call for the language it is. Java needs overloading because its type system demands a separate method signature for every parameter-type combination. Python's default arguments plus *args/**kwargs combination covers nearly every real use case overloading solves in Java, with dramatically less boilerplate โ one function, sensible defaults, done. The trade-off is that Python leans entirely on the developer's discipline to keep a function's behaviour coherent across all the ways it can be called, since there's no compiler enforcing separate, type-safe code paths for you.
Advantages and Disadvantages of Python Functions
Functions are close to a pure positive in software design, but Python's specific implementation has real trade-offs worth knowing.
Function Call Architecture โ From Call Site to Return
The diagram below shows the full stack a function call passes through โ from the moment you write the call in your code down to how CPython actually manages the call frame internally.
Writing Your First Real Function โ And the Errors Along the Way
Here's a function that looks reasonable, the exact error it produces when called incorrectly, and the corrected, properly defensive version.
def calculate_discount(price, percent):
discount = price * (percent / 100)
price - discount
final_price = calculate_discount(500, 10)
print(final_price + 50)Output
Traceback (most recent call last): File "naive_function.py", line 6, in <module> print(final_price + 50) TypeError: unsupported operand type(s) for +: 'NoneType' and 'int'The bug: price - discount is computed but never returned โ no return keyword means the function gives back None regardless. Here's the corrected version, with default arguments, a docstring, and basic validation:
def calculate_discount(price, percent=10):
"""Return the price after applying a percentage discount.
Args:
price (float): Original price before discount.
percent (float): Discount percentage, default 10.
Returns:
float: Final price after discount is applied.
"""
if price < 0 or percent < 0:
raise ValueError("price and percent must be non-negative")
discount = price * (percent / 100)
return round(price - discount, 2)
final_price = calculate_discount(500, 10)
print(final_price + 50)
# Using the default percent value
print(calculate_discount(200))Output
500.0 180.0Practice This Code โ Live Editor
Line-by-Line Explanation
- โถ
def calculate_discount(price, percent=10):โpercenthas a default value, making it optional.pricehas no default, so it remains required โ every call must supply it. - โถ
"""..."""right after the def line โ This is the function's docstring. It's not a comment; it's an actual string object accessible later viacalculate_discount.__doc__or Python's built-inhelp()function. - โถ
raise ValueError(...)โ Validates inputs before doing any real work. This is defensive programming: catching bad input early with a clear error, rather than letting a nonsensical negative discount silently produce a nonsensical result. - โถ
return round(price - discount, 2)โ The fix for the earlier bug. This line both computes AND hands back the final value, stopping the function immediately after. - โถ
calculate_discount(200)โ Called with only one argument. Python uses the defaultpercent=10automatically since no second value was supplied.
Where Functions Actually Matter โ Real-World Applications
Functions aren't a classroom-only abstraction โ they're the fundamental unit of organisation in every serious Python codebase, across every domain:
- โถ
๐ Web Backend Route Handlers โ In Django and Flask, every URL route ultimately maps to a function (or method) that receives a request and returns a response. Understanding parameters and return values directly translates into understanding how web frameworks route and respond to requests.
- โถ
๐ค Machine Learning Pipelines โ TensorFlow and PyTorch code is built almost entirely from functions โ preprocessing functions, loss functions, training-step functions โ chained together. A model training script is fundamentally a sequence of well-named function calls.
- โถ
โ๏ธ Automation and CLI Scripts โ Every reusable ops script โ backup_database(), send_alert(), cleanup_old_logs() โ is a function with a clear job, called from a main() function that orchestrates the overall script flow.
- โถ
๐งช Unit Testing โ pytest and unittest are built around calling small, focused functions and asserting on their return values. Code that isn't broken into functions is, practically speaking, code that can't be unit tested at all.
- โถ
๐ Data Analysis and Transformation โ Pandas-heavy scripts typically define functions like clean_column(df, name) or compute_summary_stats(df) to keep data-cleaning logic readable and reusable across multiple datasets.
- โถ
๐ Authentication and Security Logic โ Functions like hash_password(), verify_token(), and check_permissions() isolate security-critical logic into single, auditable, testable units rather than scattering security checks inline throughout a codebase.
- โถ
๐ฎ Game Development โ Pygame-based games are organised around functions like update_player_position(), check_collision(), and render_frame(), typically called repeatedly inside a main game loop.
- โถ
โ๏ธ Serverless / Cloud Functions โ AWS Lambda, Google Cloud Functions, and Azure Functions are literally named after this concept โ a single Python function is the entire deployable unit, triggered by an event and returning a response.
Why Functions Are the Real Turning Point in Learning Python
Variables, loops, and conditionals teach you syntax. Functions teach you how to actually design software. Here's why this topic deserves more attention than it usually gets:
- โถ
๐ง It's the First Real Abstraction Skill โ Deciding what a function should do, what it should be named, and what it should return is genuine software design โ the first point where 'writing code that runs' and 'writing code that's good' start to diverge.
- โถ
๐ผ Interviewers Test This Relentlessly โ 'Write a function that...' is the opening line of an enormous share of coding interview questions. Fluency with parameters, defaults, and return values isn't optional for interview prep โ it's the baseline.
- โถ
๐ It Eliminates a Whole Category of Bugs โ The copy-paste-and-forget-to-update-all-copies bug class disappears almost entirely once logic lives in one function called from multiple places instead of duplicated inline.
- โถ
๐งช It's the Foundation of Testable Code โ Nothing in software engineering practice matters more directly to code quality than whether your logic is broken into small, testable, well-named functions.
- โถ
๐ง It Unlocks Every Advanced Python Feature โ Decorators, generators, async/await, context managers โ every one of these is built on top of function fundamentals. Shaky function understanding makes every subsequent topic harder than it needs to be.
- โถ
๐ฎ๐ณ It's the Core Skill Tested in Indian Placement Drives โ Campus placement coding rounds and early technical interviews at Indian product and service companies alike lean heavily on 'write a function to solve X' problems โ this is the single most rehearsed skill in that entire process.
Special Kinds of Functions Worth Knowing
Beyond the standard def function, Python has several specialised function forms, each solving a specific, narrow problem:
- โถ
lambda โ Anonymous, Single-Expression Functions โ sorted(students, key=lambda s: s['marks']) creates a throwaway function inline, without a name, limited to exactly one expression. Great for short logic passed to sort(), map(), or filter(); a poor choice for anything requiring multiple statements or clarity.
- โถ
Recursive Functions โ Calling Themselves โ def factorial(n): return 1 if n == 0 else n * factorial(n - 1) solves a problem by breaking it into a smaller version of itself. Elegant for tree structures and mathematical definitions, but watch out for Python's default recursion limit and RecursionError on genuinely deep recursion.
- โถ
Generator Functions โ yield Instead of return โ A function using yield instead of return produces values lazily, one at a time, pausing between each โ ideal for processing large sequences without loading everything into memory at once.
- โถ
Nested Functions & Closures โ A function defined inside another function can 'close over' variables from the outer scope, remembering them even after the outer function has finished executing. This is the mechanism decorators are built on.
- โถ
async def โ Coroutine Functions โ Defined with async def and called with await inside an event loop, these functions can pause and resume, enabling concurrent I/O-bound operations like multiple simultaneous network requests without blocking threads.
- โถ
Higher-Order Functions โ Any function that accepts another function as a parameter or returns one as a result. map(), filter(), and every decorator in Python are higher-order functions by this definition.
Python Functions โ Interview Questions
These come up in essentially every Python interview, from campus placements to mid-level backend screening rounds. Know these precisely, not approximately.
Practice Questions โ Test Your Understanding
Work through these before checking the answers. Predicting the exact output โ or exact error message โ is the real test of whether function mechanics have actually clicked.
1. What is the output of: def f(): pass \n print(f())
Easy2. What happens when you call greet() if the function is defined as def greet(name='World'): return f'Hello, {name}!'?
Easy3. Trace through this: def add_item(item, basket=[]): basket.append(item); return basket โ then call add_item('apple') followed by add_item('banana'). What does the second call return?
Hard4. What's the exact error from calling def calculate(price, tax): return price + tax as calculate(100)?
Easy5. What does this print? def outer(): x = 10 \n def inner(): print(x) \n inner() \n outer()
Medium6. Why does this raise an error? def counter(): count = 0 \n def increment(): count += 1 \n increment() \n counter()
Hard7. What is the output of: numbers = [3, 1, 4, 1, 5]; print(sorted(numbers, key=lambda x: -x))?
Medium8. What happens if a recursive function like def countdown(n): print(n); countdown(n - 1) is called with countdown(5), and what's missing?
MediumConclusion โ Functions Are Where Programming Actually Starts
Everything before functions โ variables, loops, conditionals โ is vocabulary. Functions are where you start writing actual sentences, and eventually, actual arguments. The jump from 'a script that runs top to bottom' to 'a collection of well-named, well-tested, reusable functions calling each other' is the single biggest shift in how your code reads, how easily it's debugged, and how confidently other people (including future you) can work with it.
If you are a complete beginner, the priority is getting parameters, return values, and default arguments to feel automatic โ not something you have to consciously think through every time. If you're moving toward real projects, the priority shifts to design: what should this function actually be responsible for, what should it be named, and what's the smallest, clearest interface it can expose to the rest of your code.
The habit worth building from here: every time you write a function, ask what it's actually responsible for, in one sentence. If the sentence needs an 'and' in it โ 'validates the input AND saves it to the database AND sends a confirmation email' โ that's usually a sign the function should be three smaller functions instead of one large one. This single habit, more than any syntax detail in this guide, is what separates code that stays maintainable as a project grows from code that quietly turns into a mess six months in.
Functions are the smallest real unit of software design in Python. Get comfortable naming them well, scoping them tightly, and returning exactly what they promise โ everything else in the language builds on top of this. ๐