Lesson 15 • Advanced
Advanced Functions & Parameters Masterclass
Master every advanced Python function technique — from closures and decorators to metaprogramming, function factories, and production-level patterns used by FAANG engineers.
What You'll Learn in This Lesson
- • Positional vs keyword arguments and the mutable default trap
- •
*argsand**kwargsfor flexible function signatures - • First-class functions: passing, returning, and storing functions
- • Closures, lambda functions, and function factories
- • Keyword-only arguments and argument unpacking with
*and**
🎯 What Makes Python Functions Special?
🏠 Real-World Analogy:
In Python, functions are like physical tools you can hold. You can put them in a toolbox (list), hand them to a friend (pass as argument), get them back (return value), or even create new tools on the fly!
| Capability | What It Means | Example |
|---|---|---|
| First-class objects | Functions are values like numbers | f = print |
| Assignable to variables | Store in a variable for later | greet = say_hello |
| Storable in collections | Keep functions in lists/dicts | ops = [add, sub] |
| Passable as arguments | Give to other functions | map(func, data) |
| Returnable as values | Functions can create functions | return inner_func |
| Capture outer scope | Remember variables (closures) | nonlocal count |
💡 Why This Matters: This gives Python functional power similar to JavaScript—enabling decorators, callbacks, and advanced patterns.
🧠 2. Positional vs Keyword Parameters (Deep Dive)
Positional vs Keyword Arguments
Compare positional and keyword arguments in Python functions
# Positional vs Keyword Arguments
# Positional arguments - order matters
def greet(name, age):
print(name, age)
greet("Alice", 25)
# Keyword arguments - order doesn't matter
greet(name="Alex", age=20)
greet(age=20, name="Alex")
# Keyword arguments improve:
# ✔ clarity
# ✔ safety
# ✔ order-independent calls
# ✔ API designKeyword arguments improve:
- ✔ clarity
- ✔ safety
- ✔ order-independent calls
- ✔ API design
⚙️ 3. Default Parameters (Correct & Incorrect Ways)
⚠️ The Mutable Default Trap (VERY Common Bug!)
One of Python's most notorious gotchas! Default values are created once when the function is defined, not each time it's called. This causes bugs with mutable defaults like lists or dicts.
| Default Type | Safe? | Why? |
|---|---|---|
x=5 (int) | ✅ Yes | Immutable - can't be changed |
x="hello" (str) | ✅ Yes | Immutable - can't be changed |
x=[] (list) | ❌ DANGER | Mutable - shared across all calls! |
x= (dict) | ❌ DANGER | Mutable - shared across all calls! |
x=None | ✅ Yes | The correct fix for mutable defaults |
✔ Correct (immutable default):
Correct Default Parameters
Using immutable default parameter values
def create_user(role="guest"): # String is immutable - safe!
return {"role": role}
print(create_user()) # {'role': 'guest'}
print(create_user("admin")) # {'role': 'admin'}❌ Dangerous default (THE BUG):
Dangerous Mutable Default
Why mutable default arguments cause bugs
def add_item(item, items=[]): # ❌ List created ONCE when function defined!
items.append(item)
return items
print(add_item(1)) # [1] - looks fine...
print(add_item(2)) # [1, 2] - WAIT, where did 1 come from?!
print(add_item(3)) # [1, 2, 3] - They all share the SAME list!✔ The Fix (use None pattern):
Fixed Mutable Default
The correct way to handle mutable defaults
def add_item(item, items=None): # ✅ None is safe
if items is None:
items = [] # Create NEW list each time
items.append(item)
return items
print(add_item(1)) # [1]
print(add_item(2)) # [2] - Fresh list each time!
print(add_item(3)) # [3] - Each call is independent[], , or any mutable object as a default. Always use None and create the object inside the function.🔄 4. *args (Variadic Positional Parameters)
🤔 What is *args?
*args lets a function accept any number of positional arguments. They get collected into a tuple you can iterate over.
*args - Variadic Positional Parameters
Accept any number of positional arguments
def total(*numbers): # *numbers catches ALL positional args
print(f"Received: {numbers}") # It's a tuple!
return sum(numbers)
print(total(1, 2, 3)) # Can pass 3 args
print(total(5, 10, 15, 20)) # Or 4 args
print(total(100)) # Or just 1!🔧 5. **kwargs (Variadic Keyword Parameters)
🤔 What is **kwargs?
**kwargs lets a function accept any number of keyword arguments. They get collected into a dictionary.
**kwargs - Variadic Keyword Parameters
Accept any number of keyword arguments
def build_user(**info): # **info catches ALL keyword args
print(f"Received: {info}") # It's a dictionary!
return info
user = build_user(name="Alex", age=20, role="admin")
print(user) # {'name': 'Alex', 'age': 20, 'role': 'admin'}| Syntax | Collects | Stored As | Example Call |
|---|---|---|---|
*args | Positional arguments | Tuple | f(1, 2, 3) |
**kwargs | Keyword arguments | Dictionary | f(a=1, b=2) |
*args, **kwargs | Both! | Tuple + Dict | f(1, 2, a=3) |
🧬 6. Argument Unpacking ( * and ** )
List unpacking:
List Unpacking with *
Unpack lists into function arguments
nums = [1, 2, 3]
print(*nums) # 1 2 3
def add(a, b, c):
return a + b + c
print(add(*nums))Dict unpacking:
Dict Unpacking with **
Unpack dictionaries into keyword arguments
data = {"x": 1, "y": 2}
def show(x, y):
print(x, y)
show(**data)This is deeply used in:
- ✔ machine learning pipelines
- ✔ data transformations
- ✔ passing parameters through layers
- ✔ functional programming
🧩 7. First-Class Functions & Higher-Order Functions
A function passed into another function:
Higher-Order Functions
Pass functions as arguments to other functions
def apply(fn, value):
return fn(value)
print(apply(lambda n: n*n, 5)) # 25
def double(x):
return x * 2
print(apply(double, 10)) # 20Used in:
- ✔ AI model callbacks
- ✔ middleware
- ✔ map/filter/reduce
- ✔ backend hooks
- ✔ schedulers
🧠 8. Lambda Functions (Real Usage)
Beginners think lambdas are "inline shortcuts". Experts know lambdas power:
- ✔ sorting
- ✔ functional pipelines
- ✔ small stateless transformations
Example:
Lambda Functions for Sorting
Use lambda functions as sorting keys
users = [{"name": "Alex", "age": 20}, {"name": "Sam", "age": 30}]
sorted_users = sorted(users, key=lambda u: u["age"])
print(sorted_users)🔒 9. Closures (Python's Most Important Function Feature)
🏠 Real-World Analogy:
A closure is like a backpack that a function carries. When you create a function inside another function, the inner function "packs" any variables it needs from the outer function and keeps them forever—even after the outer function finishes!
| Term | Meaning |
|---|---|
| Closure | A function that "remembers" variables from where it was created |
nonlocal | Keyword to modify (not just read) an outer variable |
| Free variable | A variable used in a function but defined outside it |
Closures with nonlocal
Create stateful functions using closures
def counter():
count = 0 # This is the "backpack" variable
def inc():
nonlocal count # ← "I want to MODIFY count, not create a new one"
count += 1
return count
return inc # Return the inner function (with its backpack!)
# Create a counter - it remembers its own count
c = counter()
print(c()) # 1 - count is remembered!
print(c()) # 2 - still remembering!
print(c()) # 3 - it persists!
# Create another counter - it has its OWN backpack
c2 = coun
...nonlocal, Python would create a NEW local variable instead of modifying the outer one. Use nonlocal when you need to change an outer variable, not just read it.Used for:
- ✔ stateful utilities (counters, accumulators)
- ✔ caching (remember previous results)
- ✔ function factories (create customized functions)
- ✔ rate limiters (track call history)
- ✔ object-like behaviour without classes
🎁 10. Decorators (The Real Advanced-Level Skill)
Decorators transform functions and classes — used everywhere in Python frameworks.
Basic Decorator Pattern
Create a logging decorator
def logger(fn):
def wrapper(*args, **kwargs):
print("Calling:", fn.__name__)
return fn(*args, **kwargs)
return wrapper
@logger
def greet():
print("Hello!")
greet()Decorators provide:
- ✔ logging
- ✔ authentication
- ✔ rate limiting
- ✔ caching
- ✔ measuring execution time
- ✔ ORM mappings
- ✔ FastAPI / Flask route handling
This is one of Python's signature advanced features.
🚀 11. Function Factories (Dynamic Function Creation)
Function Factories
Create functions dynamically with closures
def multiply_by(n):
def inner(x):
return n * x
return inner
double = multiply_by(2)
triple = multiply_by(3)
print(double(10)) # 20
print(triple(10)) # 30Used in:
- ✔ ML preprocessing
- ✔ generating optimised functions
- ✔ parameter-controlled utilities
- ✔ dynamic pipelines
🔁 12. Recursion (Pythonic Patterns)
Classic example:
Recursive Factorial
Classic recursion example with factorial
def factorial(n):
return 1 if n <= 1 else n * factorial(n-1)
print(factorial(5)) # 120Real use cases:
- ✔ tree search
- ✔ JSON traversal
- ✔ directory crawling
- ✔ AI state exploration
- ✔ compilers and parsers
Tail recursion isn't optimised in Python—so you must use it strategically.
🧮 13. Memoization (Caching for High Performance)
🤔 What is Memoization?
Memoization means remembering the results of expensive function calls. If you call the function with the same inputs again, it returns the cached result instantly instead of recalculating.
| Without Memoization | With Memoization |
|---|---|
fib(30) → 1+ million calculations | fib(30) → ~30 calculations |
| Exponential time O(2ⁿ) | Linear time O(n) |
| Takes seconds/minutes | Instant ⚡ |
Memoization with lru_cache
Cache function results for performance
from functools import lru_cache
@lru_cache(maxsize=None) # ← This one line adds caching!
def fib(n):
if n < 2:
return n
return fib(n-1) + fib(n-2)
# Without @lru_cache, fib(30) would take forever
# With it, it's instant!
print(fib(30)) # 832040 - calculated instantly!
print(fib(50)) # 12586269025 - still instant!@lru_cache is Python's built-in memoization. Use maxsize=None for unlimited cache, or maxsize=128 to limit memory usage.Used everywhere in:
- ✔ AI & dynamic programming algorithms
- ✔ data pipelines with repeated queries
- ✔ expensive calculations (math, crypto)
- ✔ API caching (avoid repeated network calls)
🧱 14. Pure vs Impure Functions
Pure:
Pure Function
Functions with no side effects
def add(a, b):
return a + b
print(add(5, 3))Impure:
Impure Function
Functions that modify external state
x = 0
def inc():
global x
x += 1
inc()
print(x)Pure functions improve:
- ✔ debugging
- ✔ testing
- ✔ parallel execution
- ✔ reliability
🎛 15. Context Managers as Functionality
Context managers extend function behavior beyond decorators.
Context Manager Timer
Create a timing context manager
from contextlib import contextmanager
@contextmanager
def timer():
import time
start = time.time()
yield
print(f"Elapsed: {time.time() - start}s")
with timer():
sum(range(1000000))Used for:
- ✔ file streams
- ✔ database sessions
- ✔ API connections
- ✔ locking mechanisms
- ✔ resource management
🕹 16. Putting It All Together — Advanced Example
A rate-limited API wrapper using:
- ✔ closures
- ✔ decorators
- ✔ *args / **kwargs
- ✔ time
- ✔ higher-order functions
Rate Limiter Decorator
Production-level rate limiting with closures
import time
def rate_limit(max_calls, interval):
calls = []
def decorator(fn):
def wrapper(*args, **kwargs):
nonlocal calls
now = time.time()
calls = [t for t in calls if now - t < interval]
if len(calls) >= max_calls:
raise Exception("Rate limit exceeded")
calls.append(now)
return fn(*args, **kwargs)
return wrapper
return decorator
@rate_limit(3, 5)
def fetch_data():
pri
...This is real production-level engineering — used in APIs, SaaS, AI tools, and infra services.
🧩 17. Currying & Partial Function Application
🤔 What's the Difference?
| Technique | What It Does | Example |
|---|---|---|
| Currying | Transform f(a,b) into f(a)(b) | multiply(2)(3) |
| Partial | Pre-fill some arguments | double = partial(multiply, 2) |
Manual currying:
Manual Currying
Transform multi-arg function into chain of single-arg functions
def multiply(a): # First call: pass 'a'
def inner(b): # Second call: pass 'b'
return a * b # Use both!
return inner
# Create specialized functions
double = multiply(2) # "Lock in" 2 as first argument
triple = multiply(3) # "Lock in" 3 as first argument
print(double(10)) # 20 (2 * 10)
print(triple(10)) # 30 (3 * 10)Using functools.partial (easier!):
Partial Function Application
Pre-fill function arguments with functools.partial
from functools import partial
def power(base, exponent):
return base ** exponent
# Pre-fill exponent=2 → creates a "square" function
square = partial(power, exponent=2)
# Pre-fill exponent=3 → creates a "cube" function
cube = partial(power, exponent=3)
print(square(5)) # 25 (5²)
print(cube(5)) # 125 (5³)🧬 18. Function Introspection
Function Introspection
Inspect function metadata and attributes
def hello(name: str) -> str:
return f"Hello {name}"
print(hello.__name__)
print(hello.__annotations__)
print(hello.__code__.co_argcount)Used in:
- ✔ documentation generators
- ✔ ORMs
- ✔ API frameworks
- ✔ testing tools
- ✔ debuggers
🧱 19. Metaprogramming With Functions
Metaprogramming with Decorators
Modify function behavior at runtime
def modify(func):
def wrapper(*a, **kw):
result = func(*a, **kw)
return f"[Modified] {result}"
return wrapper
@modify
def greet():
return "Hello"
print(greet()) # [Modified] HelloMetaprogramming powers:
- ✔ FastAPI route injections
- ✔ Django model generation
- ✔ SQLAlchemy ORM mappings
- ✔ automatic validations
- ✔ custom DSLs
🔁 20. Function Pipelines (Functional Composition)
Function Composition
Combine functions into pipelines
def compose(f, g):
return lambda x: f(g(x))
def increment(x): return x + 1
def double(x): return x * 2
pipeline = compose(double, increment)
print(pipeline(5)) # (5+1)*2 = 12Functional composition is used in:
- ✔ ETL pipelines
- ✔ data cleaning
- ✔ ML transformations
- ✔ text processing
- ✔ audio/image pipelines
📦 21. Callables Beyond Functions
Callable Classes
Create callable objects with __call__
class Multiplier:
def __init__(self, factor):
self.factor = factor
def __call__(self, value):
return value * self.factor
double = Multiplier(2)
print(double(10)) # 20This allows:
- ✔ function-like objects with state
- ✔ ML layers (PyTorch uses this!)
- ✔ command objects
- ✔ configurable utilities
🧲 22. Dynamic Dispatch (Single Dispatch)
Single Dispatch
Type-based function overloading
from functools import singledispatch
@singledispatch
def display(x):
print("Default:", x)
@display.register(int)
def _(x):
print("Integer:", x)
@display.register(list)
def _(x):
print("List:", x)
display(10)
display([1,2,3])Why important?
- ✔ cleaner APIs
- ✔ automatic type routing
- ✔ easy overloading without OOP
⚙️ 23. Decorators With Arguments (Decorator Factories)
Decorator Factories
Create decorators that accept arguments
def repeat(n):
def wrapper(func):
def inner(*a, **kw):
for _ in range(n):
func(*a, **kw)
return inner
return wrapper
@repeat(3)
def greet():
print("Hello")
greet()This is essential for:
- ✔ authentication systems
- ✔ rate limiting
- ✔ logging levels
- ✔ retry mechanisms
- ✔ dynamic configuration
🧪 24. Creating Custom Decorators for Error Handling
Error Handling Decorator
Catch and handle exceptions in decorators
def safe(func):
def wrapper(*a, **kw):
try:
return func(*a, **kw)
except Exception as e:
print("Error:", e)
return wrapper
@safe
def risky(x):
return 10 / x
risky(0) # prints error instead of crashingUsed in:
- ✔ production pipelines
- ✔ monitoring systems
- ✔ API endpoints
- ✔ retry logic
🔍 25. Benchmarking Functions
Benchmarking Functions
Measure function execution time
import time
def slow():
sum(i*i for i in range(20000))
start = time.perf_counter()
slow()
end = time.perf_counter()
print("Time:", end - start)Essential for:
- ✔ optimisation
- ✔ ML tuning
- ✔ heavy loops
- ✔ performance regressions
🚀 26. Real-World: Dynamic API Client Generator
Dynamic API Client
Build SDK-like API clients with factories
def api_client(base_url):
def make_request(endpoint):
def call(**params):
print("GET", base_url + endpoint, params)
return call
return make_request
github = api_client("https://api.github.com")
get_user = github("/users")
get_user(username="torvalds")This is how real SDKs are created.
🧨 27. Using Functions to Create DSLs
Domain-Specific Languages
Create DSLs with function composition
def select(*fields):
return {"select": fields}
def where(**conditions):
return {"where": conditions}
query = {**select("name", "age"), **where(id=5)}
print(query)DSLs are used in:
- ✔ ORM queries
- ✔ configuration languages
- ✔ build tools
- ✔ infrastructure scripts
🧠 28. Advanced Factory Patterns With Functions
Serializer Factory
Create format-specific serializers dynamically
def serializer(fmt):
if fmt == "json":
import json
return json.dumps
if fmt == "repr":
return repr
to_json = serializer("json")
print(to_json({"key": "value"}))🎛 29. Using Functions as Middleware Chains
Middleware Chains
Process data through function chains
def middleware_chain(func_list):
def call(value):
for f in func_list:
value = f(value)
return value
return call
pipeline = middleware_chain([
lambda x: x + 1,
lambda x: x * 5,
str
])
print(pipeline(10)) # "55"Used in:
- ✔ web frameworks
- ✔ logging systems
- ✔ request processing
🧨 30. Part 1 Summary
You can now work with:
- ✔ closures
- ✔ decorators
- ✔ currying
- ✔ pipelines
- ✔ introspection
- ✔ function factories
- ✔ recursion & memoization
- ✔ pure/impure patterns
- ✔ middleware
- ✔ advanced dispatch
- ✔ context managers
🧠 31. Descriptors — The Hidden Power Behind Properties
Python Descriptors
Control attribute access with descriptors
class Descriptor:
def __get__(self, instance, owner):
return "Got value"
def __set__(self, instance, value):
print("Set:", value)
class Demo:
x = Descriptor()
d = Demo()
print(d.x)
d.x = 10Descriptors power:
- ✔ @property
- ✔ dataclasses
- ✔ ORM fields (Django, SQLAlchemy)
- ✔ class-level validators
- ✔ computed attributes
🔧 32. Metaclasses + Functions = Dynamic Class Construction
Metaclasses
Dynamic class creation with metaclasses
class Meta(type):
def __new__(cls, name, bases, attrs):
attrs['created_by'] = 'Meta'
return super().__new__(cls, name, bases, attrs)
class User(metaclass=Meta):
pass
print(User.created_by)Used in:
- ✔ Django ORM
- ✔ Pydantic
- ✔ SQLAlchemy
- ✔ DRF serializers
- ✔ TensorFlow layers
🎛 33. Using Functions to Build Plugins
Plugin Architecture
Build extensible plugin systems
PLUGINS = {}
def plugin(name):
def register(func):
PLUGINS[name] = func
return func
return register
@plugin("compress")
def compress(data):
return data[:10]
print(PLUGINS)Plugins are used in:
- ✔ VS Code extensions
- ✔ Blender addons
- ✔ Flask extensions
- ✔ AI model hooks
- ✔ Code formatters
🧩 34. Higher-Order Error Handling Patterns
Higher-Order Error Handling
Create flexible error handling decorators
def rescue(default=None):
def wrapper(func):
def inner(*a, **kw):
try: return func(*a, **kw)
except: return default
return inner
return wrapper
@rescue(default="failed")
def risky(x):
return 10/x
print(risky(0)) # "failed"Advanced error-handling patterns:
- ✔ retry decorators
- ✔ exponential backoff
- ✔ circuit breakers
- ✔ fallbacks
- ✔ graceful degradation
🧬 35. Partial Evaluation & Runtime Specialisation
Partial Evaluation
Specialize functions at runtime
from functools import partial
def logistic(x, L, k, x0):
return L / (1 + 2.71828 ** (-k*(x-x0)))
fast_logistic = partial(logistic, L=1, k=0.5)
print(fast_logistic(10, x0=5))Partial evaluation is used in:
- ✔ machine learning
- ✔ neural network training loops
- ✔ optimisation algorithms
- ✔ scientific computing
- ✔ compiler design
⚡ 36. Memoization Variants (Custom TTL Cache)
TTL Cache Decorator
Create time-based caching with decorators
import time
def ttl_cache(seconds):
def wrapper(func):
store = {}
def inner(*a):
now = time.time()
if a in store:
value, t = store[a]
if now - t < seconds:
return value
result = func(*a)
store[a] = (result, now)
return result
return inner
return wrapper
@ttl_cache(5)
def expensive():
return "computed"This is the basis of:
- ✔ caching middleware
- ✔ API clients
- ✔ ML model caching
- ✔ search engines
🧱 37. Turning Functions Into Stateful Machines
Stateful Closures
Create stateful functions without classes
def counter():
n = 0
def inner():
nonlocal n
n += 1
return n
return inner
c = counter()
print(c()) # 1
print(c()) # 2Stateful function techniques create:
- ✔ batching systems
- ✔ throttlers
- ✔ generators
- ✔ stream processors
- ✔ real-time systems
🧠 38. Function Composition for Data Pipelines
Data Pipeline Composition
Build reusable data transformation pipelines
def compose(*functions):
def pipeline(value):
for f in functions:
value = f(value)
return value
return pipeline
clean = compose(
lambda x: x.strip(),
lambda x: x.lower(),
lambda x: x.replace("!", "")
)
print(clean(" HELLO! "))Used in:
- ✔ Pandas transformations
- ✔ NLP preprocessing
- ✔ audio pipelines
- ✔ AI data augmentation
🧲 39. Callback Systems (Events, Hooks & Signals)
Callback Systems
Build event-driven architectures
callbacks = []
def on_event(func):
callbacks.append(func)
return func
@on_event
def notify():
print("Notified!")
for f in callbacks:
f()Callbacks are the foundation of:
- ✔ GUI systems
- ✔ Async programming
- ✔ Game engines
- ✔ ML training loops
- ✔ robotics
🧨 40. Advanced Use of yield for Coroutines
Generator Coroutines
Use yield for two-way communication
def processor():
total = 0
while True:
x = yield total
total += x
p = processor()
next(p)
print(p.send(5)) # 5
print(p.send(3)) # 8This enables:
- ✔ continuous data processing
- ✔ actor-like systems
- ✔ streaming architectures
- ✔ log processors
- ✔ incremental ML pipelines
🧮 41. Decorators That Modify Function Signatures
Signature Modification
Inspect and modify function signatures
from functools import wraps
import inspect
def rename_args(**new_names):
def wrapper(func):
sig = inspect.signature(func)
print("Original sig:", sig)
return func
return wrapper
@rename_args(x="value")
def test(x):
passThis kind of technique builds:
- ✔ Pydantic
- ✔ FastAPI
- ✔ click (CLI library)
- ✔ Flask request routing
🎛 42. Building an Advanced Pipeline Framework
Pipeline Framework
Build a reusable pipeline framework
class Pipeline:
def __init__(self):
self.steps = []
def step(self, func):
self.steps.append(func)
return func
def run(self, input):
for f in self.steps:
input = f(input)
return input
p = Pipeline()
@p.step
def clean(x): return x.strip()
@p.step
def lower(x): return x.lower()
print(p.run(" HELLO "))This mirrors real frameworks like Airflow or spaCy.
🎉 Final Wrap-Up
You now understand every advanced Python function mechanism:
🧠 High-Level Concepts
- ✔ first-class functions
- ✔ closures
- ✔ decorators
- ✔ lambda pipelines
- ✔ recursion
- ✔ functional composition
⚙️ Engineering Concepts
- ✔ metaprogramming
- ✔ introspection
- ✔ plugin architecture
- ✔ partial evaluation
- ✔ advanced caching
- ✔ pipeline engines
🧨 Low-Level Power
- ✔ descriptors
- ✔ metaclasses
- ✔ dynamic function creation
- ✔ stateful closures
- ✔ coroutine interaction
- ✔ generator processors
You've reached a level of Python function mastery that only senior engineers, framework authors, or ML system architects typically achieve.
📋 Quick Reference — Advanced Functions
| Syntax | What it does |
|---|---|
| *args | Collect extra positional arguments |
| **kwargs | Collect extra keyword arguments |
| functools.partial(fn, x) | Fix one or more arguments |
| inspect.signature(fn) | Introspect function parameters |
| lambda x: x * 2 | Create an inline anonymous function |
🏆 Lesson Complete!
You've mastered every advanced function technique Python has — from variadic arguments to partial application and function introspection.
Up next: Higher-Order Functions — pass and return functions to build powerful pipelines.
Sign up for free to track which lessons you've completed and get learning reminders.