Django, the Python web framework that powers Spotify, Pinterest, and Disqus, exposes every view function with flexible signatures that accept *args and **kwargs, letting thousands of third-party developers add middleware, authentication, and caching to any endpoint without modifying core framework code. This flexibility is what allows a framework to serve both a 10-page startup site and Spotify's 600 million users with the same codebase. The function signature patterns you are about to learn are the foundation of that extensibility.
Default Parameters
Daily Life
Interviews
Design functions with smart fallbacks
Default parameters let you specify fallback values for function arguments. When a caller omits an argument, Python uses the default. This makes functions more flexible by allowing optional parameters without requiring the caller to always provide them.
Basic Default Values
Define defaults using parameter=value in the function signature:
1
defgreet(name,greeting="Hello"):
2
returngreeting+", "+name+"!"
3
4
# Call with both arguments
5
print(greet("Alice","Hi"))
6
7
# Call with only required argument
8
print(greet("Bob"))
9
10
# Call with only required argument
11
print(greet("Charlie"))
>>>Output
Hi, Alice!
Hello, Bob!
Hello, Charlie!
The greeting parameter has a default value of "Hello". When you call greet("Bob"), Python automatically uses "Hello" because you did not provide a second argument. This simple mechanism unlocks tremendous flexibility in function design.
Default parameters are evaluated left to right at function definition time, not at call time. This distinction becomes important when we discuss the mutable default pitfall later in this section. For now, understand that each call either uses your provided value or falls back to the pre-defined default.
Multiple Default Parameters
Functions can have multiple default parameters. This is common in data processing functions where you want sensible defaults:
SELECT * FROM users ORDER BY id LIMIT 100 OFFSET 0
SELECT * FROM users ORDER BY id LIMIT 50 OFFSET 0
SELECT * FROM orders ORDER BY id LIMIT 25 OFFSET 100
Notice how the function remains useful with minimal arguments. The caller only needs to specify the table name, and sensible defaults handle pagination and sorting. This pattern appears constantly in database utilities, API clients, and data processing libraries.
TIP
In production ETL code, default parameters are everywhere. Think of batch sizes, timeout values, retry counts, and date ranges. Good defaults make your functions immediately usable without configuration.
Here are examples of the most common default parameters you will see in data engineering code.
limit=100timeout=30retries=3verbose=Futf-8
limit=100
Pagination
Control batch size easily
timeout=30
Timeouts
Sensible wait by default
retries=3
Retry logic
Auto-retry on failure
verbose=F
Debug flags
Quiet unless turned on
utf-8
File encoding
Standard text encoding
Keyword Args with Defaults
When you have multiple defaults, use keyword arguments to skip middle parameters:
Keyword arguments let you specify exactly which parameters to override. This is much cleaner than passing values for every parameter just to change one. Without keyword arguments, you would need to remember the position of every parameter and provide values for all parameters before the one you want to change.
This flexibility is why Python APIs are often more readable than those in languages without keyword arguments. When you see connect_db(timeout=60), the intent is immediately clear. Compare this to connect_db("localhost", 5432, 60, True) where you need to consult documentation to understand what 60 and True mean.
Required vs Optional Params
Parameters without defaults are required. Required parameters must come before parameters with defaults:
Never use mutable objects (lists, dicts) as default values. They are created once when the function is defined, not on each call:
1
# WRONG: Mutable default (list)
2
defadd_item_bad(item,items=[]):
3
items.append(item)
4
returnitems
5
6
# Each call shares the SAME list!
7
print(add_item_bad("a"))
8
print(add_item_bad("b"))
9
print(add_item_bad("c"))
>>>Output
['a']
['a', 'b']
['a', 'b', 'c']
The list accumulates across calls because all calls share the same default list object. This is one of Python's most common gotchas.
1
# CORRECT: Use None as default
2
defadd_item_good(item,items=None):
3
ifitemsisNone:
4
items=[]
5
items.append(item)
6
returnitems
7
8
# Each call gets a fresh list
9
print(add_item_good("a"))
10
print(add_item_good("b"))
11
print(add_item_good("c"))
12
13
# Can still pass existing list
14
existing=["x","y"]
15
print(add_item_good("z",existing))
>>>Output
['a']
['b']
['c']
['x', 'y', 'z']
✓Do
Use None as default, create mutable object inside
Use immutable defaults: strings, numbers, tuples
Test functions with repeated calls to catch shared state
✗Don't
Use [] or {} as default parameter values
Assume each call gets a fresh default list or dict
Ignore this in interviews - it is a classic gotcha
This pattern extends to any mutable type: lists, dictionaries, sets, and even custom objects. Safe defaults are immutable values like None, integers, floats, strings, tuples, and booleans. Dangerous defaults are mutable types like lists, dicts, sets, and custom objects. The None sentinel pattern is so common that experienced Python developers recognize it immediately.
Explore what happens when you use a mutable default versus the safe None pattern. Toggle the default value below and run both scenarios.
Fill in the Blank
> A function adds an item to a list using a default parameter. Pick None or [] as the default and see whether items accumulate across calls or stay independent.
def add_item(item, items=):
if items is None:
items = []
items.append(item)
return items
print(add_item("a"))
print(add_item("b"))
The None sentinel pattern is the standard Python idiom for any mutable default. Using None signals clearly to readers that the function creates a fresh container each time it is called.
This bug is especially dangerous in data pipelines because functions that process batches may accumulate state between calls. A function that was correct for the first batch silently produces wrong results for every subsequent one.
Immutable defaults like integers, strings, booleans, and tuples are always safe. When you need a mutable default, always use None and initialize inside the function body.
Multiple Return Values
Daily Life
Interviews
Return and unpack multiple results
Python functions can return multiple values by returning a tuple. The caller can then unpack these values into separate variables. This pattern is cleaner than returning a dictionary or list when you have a fixed number of related values to compute and return together.
In data engineering, you often need to compute several related metrics from the same data in a single pass. Rather than calling separate functions (which would iterate over the data multiple times), you compute everything in one function and return all the results. This is more efficient and keeps related logic together.
Returning Multiple Values
Separate return values with commas. Python automatically wraps them in a tuple:
1
defget_stats(numbers):
2
total=sum(numbers)
3
count=len(numbers)
4
average=total/countifcount>0else0
5
returntotal,count,average
6
7
# Unpack into separate variables
8
data=[10,20,30,40,50]
9
total,count,avg=get_stats(data)
10
11
print("Total:",total)
12
print("Count:",count)
13
print("Average:",avg)
>>>Output
Total: 150
Count: 5
Average: 30.0
The line return total, count, average creates and returns a tuple (150, 5, 30.0). The unpacking total, count, avg = get_stats(data) splits it back into variables.
Multi-Return Patterns
Multiple returns are common when processing data and computing related metrics:
sample="Python is a powerful programming language"
9
words,chars,avg_len=analyze_text(sample)
10
11
print("Words:",words)
12
print("Characters:",chars)
13
print("Avg word length:",round(avg_len,1))
>>>Output
Words: 6
Characters: 42
Avg word length: 7.0
Ignoring Unwanted Values
Use underscore _ to ignore values you do not need:
1
defget_user_info():
2
return"Alice",28,"Seattle","Engineer"
3
4
# Only need name and city
5
name,_,city,_=get_user_info()
6
print(name+" lives in "+city)
7
8
# Only need the first value
9
name,*_=get_user_info()
10
print("User:",name)
>>>Output
Alice lives in Seattle
User: Alice
The underscore is a convention meaning "I do not care about this value." The *_ syntax captures all remaining values into a throwaway list.
Keeping as Tuple
You can also receive all values as a single tuple if needed:
1
defmin_max(numbers):
2
returnmin(numbers),max(numbers)
3
4
data=[3,1,4,1,5,9,2,6]
5
6
# Keep as tuple
7
result=min_max(data)
8
print("Result tuple:",result)
9
print("Min:",result[0])
10
print("Max:",result[1])
11
12
# Or unpack immediately
13
low,high=min_max(data)
14
print("Range:",low,"to",high)
>>>Output
Result tuple: (1, 9)
Min: 1
Max: 9
Range: 1 to 9
TIP
When a function returns more than 3-4 values, consider using a named tuple or dataclass instead. It makes the code more readable and self-documenting.
Extended Unpacking
Python 3 introduced extended unpacking with * to capture multiple values into a list. This is useful when functions return variable-length results:
1
defget_scores():
2
return95,87,92,78,88,91
3
4
# Get first, last, and middle
5
first,*middle,last=get_scores()
6
print("First:",first)
7
print("Middle:",middle)
8
print("Last:",last)
9
10
# Get first two and rest
11
top1,top2,*others=get_scores()
12
print()
13
print("Top two:",top1,top2)
14
print("Others:",others)
>>>Output
First: 95
Middle: [87, 92, 78, 88]
Last: 91
Top two: 95 87
Others: [92, 78, 88, 91]
The starred variable captures all values not assigned to other variables. This is particularly useful when parsing data where you know the structure of the first and last elements but the middle can vary in length.
Data Validation Example
A common pattern is returning both a result and a status indicator:
This validate-and-return pattern is extremely common in data pipelines. Rather than raising exceptions for invalid data (which disrupts batch processing), you return validation status and error details. The caller can then decide how to handle invalid records: skip them, log them, or fix them.
Related statistics
Compute total, count, and average in one pass over the data.
Status plus result
Return a success flag alongside the computed data for error handling.
String splitting
Separate text into parts like (before, separator, after) tuples.
Validation results
Return is_valid boolean and a list of error messages together.
Data plus metadata
Return transformed records along with metadata like row counts.
Python Quiz
> A function returns the smallest and largest values from a list. Pick the built-in that finds the minimum and the one that finds the maximum.
Returning multiple values is a Python idiom that keeps related results together without requiring a class or a dictionary. The caller can unpack them in a single assignment, making the code concise and readable.
Tuple unpacking on the left side of an assignment is one of Python's most expressive features. It communicates the expected structure of the return value directly at the call site.
When a function must return a large number of related values, consider a named tuple or a dataclass. These provide both the convenience of tuple unpacking and the clarity of attribute access by name.
Local vs Global Scope
Daily Life
Interviews
Control variable visibility in code
Scope determines where a variable is visible and accessible. Python has two main scopes: local (inside a function) and global (module level). Understanding scope prevents bugs where variables unexpectedly share or shadow each other. Scope bugs are especially tricky because the code often looks correct but behaves differently than expected.
Every variable in Python lives in a specific scope. When you reference a variable name, Python searches scopes in a specific order to find it. Understanding this lookup order is crucial for predicting how your code will behave, especially in larger programs with many functions.
Local Scope
Variables created inside a function exist only within that function. They are created when the function runs and destroyed when it returns:
1
defcalculate():
2
x=10
3
y=20
4
result=x+y
5
print("Inside function:",result)
6
returnresult
7
8
answer=calculate()
9
print("Returned:",answer)
10
11
# x is not accessible outside
12
# print(x) # NameError
>>>Output
Inside function: 30
Returned: 30
The variables x, y, and result only exist inside calculate(). Once the function returns, they are gone. This isolation is a feature: it prevents functions from accidentally interfering with each other.
Local scope is created fresh for each function call. If you call calculate() twice, each call gets its own independent x, y, and result variables. This means recursive functions work correctly, and concurrent calls do not interfere with each other. The isolation makes functions predictable and testable.
TIP
Local variables are your friends. They make functions self-contained and predictable. You can understand what a function does by looking at just that function, without tracking global state.
Global Scope
Variables defined outside all functions are global. They can be read from anywhere in the module:
1
# Global variable
2
CONFIG_TIMEOUT=30
3
4
defget_timeout():
5
# Can READ global variables
6
returnCONFIG_TIMEOUT
7
8
defshow_config():
9
# Can also READ globals
10
print("Timeout setting:",CONFIG_TIMEOUT)
11
12
print("Direct access:",CONFIG_TIMEOUT)
13
print("Via function:",get_timeout())
14
show_config()
>>>Output
Direct access: 30
Via function: 30
Timeout setting: 30
Global variables are useful for configuration constants, database connections, and other values that should be shared across the entire module. However, reading globals is very different from modifying them. Python allows reading by default but requires explicit declaration to modify.
Variable Shadowing
When a local variable has the same name as a global, the local shadows (hides) the global inside that function:
1
value="global"
2
3
defshow_shadowing():
4
# Creates new LOCAL variable, not modifying global
5
value="local"
6
print("Inside function:",value)
7
8
defshow_global():
9
print("Reading global:",value)
10
11
show_shadowing()
12
show_global()
13
print("Outside:",value)
>>>Output
Inside function: local
Reading global: global
Outside: global
The assignment value = "local" creates a new local variable that hides the global one. The global value is never modified. This behavior protects global state from accidental modification.
Shadowing can cause confusion when you expect to read a global but accidentally create a local with the same name. Python determines scope at compile time, not runtime. If a variable is assigned anywhere in a function, Python treats it as local throughout that entire function, even before the assignment.
1
status="ready"
2
3
defcheck_status():
4
# This FAILS - Python sees 'status =' later and treats status as local
5
# print(status) # UnboundLocalError
6
# This assignment makes 'status' local to entire function
7
status="running"
8
returnstatus
9
10
# The global is never touched
11
print("Global:",status)
12
print("Function:",check_status())
13
print("Global still:",status)
>>>Output
Global: ready
Function: running
Global still: ready
The global Keyword
To modify a global variable from inside a function, declare it with the global keyword:
1
counter=0
2
3
defincrement():
4
globalcounter
5
counter=counter+1
6
print("Counter inside:",counter)
7
8
print("Before:",counter)
9
increment()
10
increment()
11
increment()
12
print("After:",counter)
>>>Output
Before: 0
Counter inside: 1
Counter inside: 2
Counter inside: 3
After: 3
✓Do
Pass values as parameters and return results
Use globals only for true constants like CONFIG
Keep functions pure and self-contained when possible
✗Don't
Modify global variables inside functions
Rely on global state that changes between calls
Use the global keyword when you can use parameters
Scope in Nested Functions
When functions are nested, inner functions can read from enclosing scopes:
1
defouter():
2
message="Hello from outer"
3
4
definner():
5
print(message)
6
7
inner()
8
returninner
9
10
outer()
>>>Output
Hello from outer
To modify a variable from an enclosing (non-global) scope, use the nonlocal keyword. This is useful for closures and factories.
•global
Module-level variables
Visible everywhere
Use sparingly
For true constants only
•nonlocal
Enclosing function scope
For nested functions
Enables closures
More controlled than global
Python follows the LEGB rule when looking up variable names. It searches four scopes in this exact order, stopping as soon as it finds a match.
01
Local
Variables defined inside the current function body.
02
Enclosing
Variables in any enclosing (outer) function scope.
03
Global
Variables defined at the module (file) level.
04
Built-in
Names pre-defined by Python like print, len, and range.
Python Quiz
> A function mutates a list by adding an element, then returns its new size. Pick the method that adds to the end, and the built-in that measures the length after the change.
Scope determines which variables a function can read and modify. Local variables are created fresh each call and disappear when the function returns. They cannot accidentally overwrite values in other parts of the program.
Mutable objects like lists are passed by reference. When you mutate a list inside a function, the changes are visible outside because both the function and the caller hold a reference to the same object.
The LEGB lookup order means Python resolves names starting from the innermost scope outward. Understanding this order lets you predict exactly which variable a name refers to, even when the same name appears in multiple scopes.
*args for Variadics
Daily Life
Interviews
Accept any number of arguments
The *args syntax lets a function accept any number of positional arguments. The arguments are collected into a tuple. This is invaluable when you do not know in advance how many values will be passed.
Think of a logging function that needs to print any number of messages, or a calculation function that sums any count of numbers, or a path builder that joins any number of directory components. Without *args, you would need to accept a list and require callers to wrap their arguments in brackets. With *args, the function call looks natural and clean.
Basic *args Usage
The asterisk * before a parameter name collects extra positional arguments:
1
defsum_all(*numbers):
2
print("Received:",numbers)
3
total=0
4
fornuminnumbers:
5
total=total+num
6
returntotal
7
8
print("Sum:",sum_all(1,2))
9
print("Sum:",sum_all(1,2,3,4,5))
10
print("Sum:",sum_all(10))
11
print("Sum:",sum_all())
>>>Output
Received: (1, 2)
Sum: 3
Received: (1, 2, 3, 4, 5)
Sum: 15
Received: (10,)
Sum: 10
Received: ()
Sum: 0
The name args is a convention. You could use *values or *items. The asterisk is what matters.
Mixing Regular and *args
You can have regular parameters before *args. Regular parameters are filled first, and *args captures the rest:
The *args pattern is used extensively in Python's standard library. Functions like print() accept any number of arguments to display. The max() and min() functions accept either a single iterable or multiple positional arguments. Understanding *args helps you use these built-in functions more effectively.
**kwargs for Keywords
Daily Life
Interviews
Handle named options as dictionaries
The **kwargs syntax captures keyword arguments into a dictionary. This allows functions to accept any named parameters, making them highly configurable without defining every possible option upfront.
Consider a function that connects to a database. Different database systems need different options: PostgreSQL might need an SSL certificate, MySQL might need a character set, and Redis might need a connection pool size. Rather than defining every possible parameter, you accept **kwargs and let callers pass whatever options their database needs.
This pattern is central to Python web frameworks. When you define a Django model or a Flask route, you pass keyword arguments that configure behavior. The framework collects these into a dictionary and processes them. Understanding **kwargs helps you both use and build such APIs.
Basic **kwargs Usage
Double asterisk ** collects keyword arguments into a dictionary:
Note how options.get("key", default) provides fallback values for missing keys. This is the standard pattern for handling optional configuration.
Forwarding Arguments
A powerful pattern is using *args and **kwargs to forward all arguments to another function:
1
deflog_call(func):
2
defwrapper(*args,**kwargs):
3
print("Calling:",func.__name__)
4
print(" args:",args)
5
print(" kwargs:",kwargs)
6
result=func(*args,**kwargs)
7
print(" result:",result)
8
returnresult
9
returnwrapper
10
11
defadd(a,b):
12
returna+b
13
14
logged_add=log_call(add)
15
logged_add(3,5)
16
print()
17
logged_add(a=10,b=20)
>>>Output
Calling: add
args: (3, 5)
kwargs: {}
result: 8
Calling: add
args: ()
kwargs: {'a': 10, 'b': 20}
result: 30
TIP
The *args/**kwargs forwarding pattern is the foundation of Python decorators. You will see this constantly in frameworks like Flask, Django, and data tools like pandas.
This forwarding pattern is how decorators preserve function signatures. The wrapper function accepts any arguments and passes them through unchanged. The decorated function receives exactly what was passed to the wrapper, regardless of its parameter structure. This makes decorators universally applicable.
Merging Dicts with **
The ** operator can merge dictionaries in function calls and dictionary literals:
This dictionary merging technique is common in configuration management. You start with default values, merge environment-specific overrides, then merge user-specified values. Each layer can override keys from previous layers while preserving keys that were not overridden.
•*args
Collects positional args
Results in a tuple
Order matters
Good for variable-length data
•**kwargs
Collects keyword args
Results in a dictionary
Named parameters
Good for configuration
Common Mistakes
These are the most frequent errors when working with intermediate function features. Each of these mistakes appears regularly in interview questions and code reviews. Understanding why they are wrong helps you avoid them in your own code and spot them in others' code.
Mistake 1: Mutable Defaults
1
# WRONG - list shared between calls
2
defappend_bad(item,items=[]):
3
items.append(item)
4
returnitems
5
6
# Default list is shared between calls!
7
print(append_bad(1))
8
print(append_bad(2))
9
10
defappend_good(item,items=None):
11
ifitemsisNone:
12
items=[]
13
items.append(item)
14
returnitems
15
16
print(append_good(1))
17
print(append_good(2))
>>>Output
[1]
[1, 2]
[1]
[2]
This is the most common intermediate Python pitfall. The list default is created once when Python parses the function definition. Every call that uses the default shares that same list object. The fix is always the same: use None as the default and create a fresh mutable object inside the function.
Mistake 2: Implicit Global
1
counter=0
2
3
# WRONG - creates local, not global
4
defincrement_wrong():
5
# UnboundLocalError: assignment makes it local
6
counter=counter+1
7
returncounter
8
9
# CORRECT - declare global explicitly
10
defincrement_right():
11
globalcounter
12
counter=counter+1
13
returncounter
14
15
# Even better - avoid globals entirely
16
defincrement_best(current):
17
returncurrent+1
18
19
value=0
20
value=increment_best(value)
21
print("Best approach:",value)
>>>Output
Best approach: 1
The UnboundLocalError is confusing because the error message says the variable is referenced before assignment, but you might think you are reading a global. The key insight is that Python determines scope at compile time based on assignments anywhere in the function, not at runtime based on execution order.
Remember the order: required positional, then *args, then keyword-only with defaults, then **kwargs. This order is enforced by Python because it is the only way to unambiguously assign arguments to parameters.
Mistake 4: Unpack First
1
defgreet(name,greeting):
2
returngreeting+", "+name
3
4
args=["Alice","Hello"]
5
6
# WRONG - passes list as single argument
7
# greet(args) # TypeError
8
9
# CORRECT - unpack list into arguments
10
print(greet(*args))
11
12
config={"name":"Bob","greeting":"Hi"}
13
# WRONG - passes dict as single argument
14
# greet(config) # TypeError
15
16
print(greet(**config))
>>>Output
Hello, Alice
Hi, Bob
This mistake is especially common when reading configuration from files or environment variables. You load a dictionary of settings and need to pass them to a function. Without the ** unpack operator, Python passes the entire dictionary as a single argument rather than expanding it into keyword arguments.
TIP
When you see a TypeError about missing arguments but you think you passed them, check whether you forgot to unpack. The error message tells you how many arguments were received versus expected.
This function tries to return multiple values but has a bug. Can you fix it by removing the extra tile?
Debug Challenge
> This function tries to return both the min and max of a list, but it uses two return keywords instead of one. Python only needs a single return with comma-separated values.
SyntaxError: only one return statement is needed to return multiple values.
The *args and **kwargs syntax enables genuinely flexible interfaces. Wrapper functions, decorators, and logging utilities all rely on forwarding arbitrary arguments without knowing their names or count in advance.
When combining *args and **kwargs with regular parameters, the order matters: required positional parameters come first, then *args, then keyword-only parameters with defaults, then **kwargs. Python enforces this order because it is the only unambiguous assignment.
The unpacking operators work in both directions. Just as * collects arguments in a function definition, it also unpacks a list or tuple at a call site. Similarly, ** collects keyword arguments in a definition and unpacks a dictionary at a call site.
❯❯❯PUTTING IT ALL TOGETHER
> You are a data engineer at Amplitude building a flexible transformation library that non-technical analysts can configure by passing custom rules without modifying the underlying pipeline code.
Default parameters give each transformation a sensible fallback so analysts get correct output without specifying every option.
return values hand back both the transformed record and a status flag so callers handle success and errors in one call.
Local vs global scope ensures analyst-supplied config variables never accidentally overwrite shared pipeline state between runs.
*args and **kwargs let analysts pass any number of filter rules or keyword overrides into a single generalized transform function.
KEY TAKEAWAYS
Default parameters use param=value syntax; required params must come first
Never use mutable defaults (lists, dicts); use None and create inside the function
Return multiple values with return a, b, c; unpack with x, y, z = func()
Local variables are isolated to their function; use global sparingly to modify globals
*args collects extra positional arguments into a tuple
**kwargs collects extra keyword arguments into a dictionary
Use *list to unpack lists and **dict to unpack dicts when calling functions
Parameter order: required, *args, keyword-only with defaults, **kwargs
The *args, **kwargs pattern enables powerful argument forwarding
Basic Default Values Default parameters are evaluated left to right at function definition time, not at call time. This distinction becomes important when we discuss the mutable default pitfall later in this section. For now, understand that each call either uses your provided value or falls back to the pre-defined default. Multiple Default Parameters Functions can have multiple default parameters. This is common in data processing functions where you want sensible defaults: Notice how the funct
Python functions can return multiple values by returning a tuple. The caller can then unpack these values into separate variables. This pattern is cleaner than returning a dictionary or list when you have a fixed number of related values to compute and return together. In data engineering, you often need to compute several related metrics from the same data in a single pass. Rather than calling separate functions (which would iterate over the data multiple times), you compute everything in one f
Scope determines where a variable is visible and accessible. Python has two main scopes: local (inside a function) and global (module level). Understanding scope prevents bugs where variables unexpectedly share or shadow each other. Scope bugs are especially tricky because the code often looks correct but behaves differently than expected. Every variable in Python lives in a specific scope. When you reference a variable name, Python searches scopes in a specific order to find it. Understanding t
Basic **kwargs Usage Using *args and **kwargs Unpacking Dictionaries Configuration Example Forwarding Arguments This forwarding pattern is how decorators preserve function signatures. The wrapper function accepts any arguments and passes them through unchanged. The decorated function receives exactly what was passed to the wrapper, regardless of its parameter structure. This makes decorators universally applicable. Merging Dicts with ** This dictionary merging technique is common in configuratio