PY

Python Fundamentals

18 lessons

Progress0%
1. Introduction to Python
1What is Python?2Setting Up Python
2. Variables and Data Types
1Variables in Python2Data Types
3. Control Flow
Conditional StatementsLoops
4. Functions
Function Basics
5. Data Structures
Lists Deep DiveTuples & SetsDictionaries & Comprehensions
6. Advanced Functions
Closures & Higher-Order FunctionsDecorators & Lambda
7. Object-Oriented Python
Classes & InstancesInheritance & Dunder Methods
8. Exception Handling
try / except / finallyCustom Exceptions & Context Managers
9. Modules & File I/O
Modules & PackagesFile I/O & JSON
All Tutorials
PythonAdvanced Functions
Lesson 12 of 18 min
Chapter 6 · Lesson 2

Decorators & Lambda

Decorators & Lambda

Lambda Functions

A lambda is a small, anonymous function defined with the lambda keyword. It is limited to a single expression:

python
square  = lambda x: x ** 2
add     = lambda x, y: x + y
no_args = lambda: "hello"

Lambdas are most useful as short callbacks passed inline to sorted, map, filter, and similar functions. For anything more complex, a named def is clearer.


Decorators

A decorator is a higher-order function that wraps another function to extend or modify its behaviour — without permanently altering the original function's source code.

The @ Syntax

python
@my_decorator
def greet():
    print("Hello!")

This is exactly equivalent to:

python
def greet():
    print("Hello!")
greet = my_decorator(greet)

Writing a Decorator

python
import functools

def timer(func):
    @functools.wraps(func)    # preserve original metadata
    def wrapper(*args, **kwargs):
        import time
        start  = time.perf_counter()
        result = func(*args, **kwargs)
        end    = time.perf_counter()
        print(f"{func.__name__} took {end - start:.4f}s")
        return result
    return wrapper

@timer
def slow_add(a, b):
    return a + b

functools.wraps

Without @functools.wraps(func), wrapper.__name__ would be "wrapper" instead of the original function's name, which breaks debugging tools and docstring inspection. Always use it.

Stacking Decorators

Multiple decorators are applied bottom-up:

python
@decorator_A
@decorator_B
def my_func():
    ...
# Equivalent to: my_func = decorator_A(decorator_B(my_func))

Decorators with Arguments

Wrap the decorator in an extra factory function:

python
def repeat(n):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            for _ in range(n):
                func(*args, **kwargs)
        return wrapper
    return decorator

@repeat(3)
def say_hi():
    print("Hi!")

*args and **kwargs Review

*args collects extra positional arguments into a tuple; **kwargs collects extra keyword arguments into a dict. Decorator wrappers use them so the inner function can accept any signature:

python
def wrapper(*args, **kwargs):
    return func(*args, **kwargs)

Code Examples

Lambda Functions in Practicepython
# Lambda as a sort key
students = [
    {"name": "Alice", "grade": 88},
    {"name": "Bob",   "grade": 95},
    {"name": "Carol", "grade": 72},
]
students.sort(key=lambda s: s["grade"], reverse=True)
for s in students:
    print(f"{s['name']}: {s['grade']}")

# Lambda with map and filter
numbers = [1, 2, 3, 4, 5, 6]
cubed   = list(map(lambda x: x**3, numbers))
odds    = list(filter(lambda x: x % 2 != 0, numbers))
print("Cubed:", cubed)
print("Odds:", odds)

Lambdas shine when you need a short throwaway function. Using a full def for s['grade'] extraction would be verbose. For multi-step logic, always prefer a named function.

Writing a Decoratorpython
import functools

def log_calls(func):
    """Decorator that logs every call to the wrapped function."""
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        arg_str  = ", ".join(str(a) for a in args)
        kwarg_str = ", ".join(f"{k}={v}" for k, v in kwargs.items())
        all_args = ", ".join(filter(None, [arg_str, kwarg_str]))
        print(f"Calling {func.__name__}({all_args})")
        result = func(*args, **kwargs)
        print(f"{func.__name__} returned {result}")
        return result
    return wrapper

@log_calls
def add(a, b):
    return a + b

@log_calls
def greet(name, greeting="Hello"):
    return f"{greeting}, {name}!"

add(3, 4)
greet("Alice", greeting="Hi")

The wrapper uses *args and **kwargs to forward all arguments transparently. functools.wraps copies the original function's __name__, __doc__, and other attributes to the wrapper, which is essential for introspection and debugging.

Stacking Decorators & Parameterised Decoratorspython
import functools

def bold(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        return "**" + func(*args, **kwargs) + "**"
    return wrapper

def uppercase(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs).upper()
    return wrapper

@bold
@uppercase
def greet(name):
    return f"hello, {name}"

print(greet("world"))  # bold( uppercase(greet) )

# Parameterised decorator
def repeat(times):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            for _ in range(times):
                result = func(*args, **kwargs)
            return result
        return wrapper
    return decorator

@repeat(3)
def say(msg):
    print(msg)

say("Python!")

Decorators are applied bottom-up: @uppercase wraps greet first, then @bold wraps the already-wrapped function. A parameterised decorator needs an extra outer function (the factory) that accepts the arguments and returns the actual decorator.

Quick Quiz

1. What does @functools.wraps(func) do inside a decorator?

2. Given @A and @B stacked above def f(), in what order are the decorators applied?

3. Which statement about lambda functions is TRUE?

Was this lesson helpful?

PreviousNext