• Python

Command Palette

Search for a command to run...

Python Fundamentals
  • Python Variables
  • Python Operators
  • Python Input-Output
  • Python Type Conversion
Python Data Types
  • Python Strings
  • Python List
  • Python Tuple
  • Python Dictionnaries
  • Python Sets
Python Flow Control
  • Python Conditions
  • Python For Loop
  • Python While Loop
  • Python Break and Continue
Python Functions
  • Python Functions
  • Python Arguments
  • Python Functions Scope
  • Python Recursion
Python Classes
  • Python Classes
  • Python Classes and Static Methods
  • Python Properties
  • Python Decorators
  • Python Error Handling

Create an Account

FREE

Join our community to access more courses.

Create Account

On this page

Adding Functionality to Functions: Understanding DecoratorsFunctions Inside Functions: Local FunctionsFunctions as First-Class ObjectsType Hinting Functions: CallableWhat Is a Decorator?Using a Decorator: The @ SyntaxHow Decorators Work (Step by Step)Writing Your Own DecoratorDecorators with ArgumentsStacking Multiple DecoratorsAdvanced Type Hinting with ParamSpecWhy Use Decorators?Common Beginner MistakesSummary
      • Pricing
      • Blog

      Python Decorators

      Adding Functionality to Functions: Understanding Decorators

      In Python, you might want to add extra features to a function - like logging, timing, or checking permissions - without changing the function's code. Decorators make this possible. They let you "wrap" a function with another function to add new behavior in a clean, reusable way.

      This article will show you what decorators are, how to use them, and how to write your own. Before we dive into decorators, let's cover some foundational concepts about functions in Python.

      Functions Inside Functions: Local Functions

      Python allows you to define functions inside other functions. These are called local functions or nested functions. They can access variables from the outer function's scope.

      Example:

      def outer_function():
          message = "Hello from the outer function!"
      
          def inner_function(): # This is a local function
              print(message) # inner_function can access message
      
          inner_function() # Call the local function from inside the outer function
      
      outer_function() # Output: Hello from the outer function!
      # inner_function() # ERROR: NameError - inner_function is not defined outside outer_function
      
      • inner_function exists only while outer_function is running.
      • Local functions are useful for tasks that are only needed within another function.

      Functions as First-Class Objects

      In Python, functions are first-class objects. This means you can treat functions like any other value (like a number or a string). You can:

      • Assign a function to a variable.
      • Pass a function as an argument to another function.
      • Return a function from another function.

      Example: Passing a Function as an Argument

      def execute_function(func):
          # execute_function takes a function 'func' as an argument
          func() # Call the function that was passed in
      
      def say_hello():
          print("Hello!")
      
      execute_function(say_hello) # Pass say_hello function as an argument
      # Output: Hello!
      

      Example: Returning a Function from Another Function

      def create_greeter(greeting):
          def greeter(name): # Define a local function
              print(f"{greeting}, {name}!")
          return greeter # Return the local function
      
      say_hi = create_greeter("Hi") # say_hi now holds the greeter function
      say_hi("Alice") # Output: Hi, Alice!
      
      say_yo = create_greeter("Yo") # say_yo holds a different greeter
      say_yo("Bob")  # Output: Yo, Bob!
      

      These abilities (local functions and treating functions as objects) are key to how decorators work.

      Type Hinting Functions: Callable

      When you use type hints, you can show that a variable or parameter is expected to be a function using the Callable type from the typing module.

      Callable is used to specify the signature of the function (the types of its arguments and its return type).

      Syntax: Callable[[ArgType1, ArgType2, ...], ReturnType]

      • [ArgType1, ArgType2, ...]: A list of the expected types of the function's arguments.
      • ReturnType: The expected return type of the function.
      • Use [] if the function takes no arguments.
      • Use Callable[..., ReturnType] if the function can take any arguments.

      Example:

      from typing import Callable
      
      # Type hint for a function that takes a string and returns an integer
      def process_text(func: Callable[[str], int], text: str) -> int:
          return func(text)
      
      def string_length(s: str) -> int:
          return len(s)
      
      # process_text expects a function that takes str and returns int
      length = process_text(string_length, "hello")
      print(length) # Output: 5
      
      # Type hint for a function that takes no arguments and returns None
      def run_task(task: Callable[[], None]) -> None:
          task()
      
      def say_done() -> None:
          print("Done!")
      
      run_task(say_done) # Output: Done!
      

      Understanding Callable helps make your code clearer when working with functions that take or return other functions.

      What Is a Decorator?

      A decorator is a function that takes another function as input and returns a new function that usually adds something before or after the original function runs. Decorators are a powerful way to extend or modify the behavior of functions (and sometimes classes) without changing their code.

      Decorators are often used for:

      • Logging
      • Timing how long a function takes
      • Checking user permissions
      • Repeating code before/after many functions

      Using a Decorator: The @ Syntax

      Python makes it easy to apply a decorator using the @decorator_name syntax, which is called a "pie" syntax because it looks like a pie.

      Example: Using a Built-in Decorator

      def shout(func: Callable[[], None]) -> Callable[[], None]:
          def wrapper() -> None:
              print("Before the function runs!")
              func()
              print("After the function runs!")
          return wrapper
      
      @shout
      def greet() -> None:
          print("Hello!")
      
      greet()
      # Output:
      # Before the function runs!
      # Hello!
      # After the function runs!
      
      • @shout applies the shout decorator to greet.
      • When you call greet(), you actually call the wrapper function inside shout.

      How Decorators Work (Step by Step)

      • You define a decorator function that takes another function as an argument.
      • Inside the decorator, you define a new function (the "wrapper") that does something extra and then calls the original function.
      • The decorator returns the wrapper function.
      • When you use @decorator, Python replaces the original function with the wrapper.

      Writing Your Own Decorator

      Example: A Simple Logging Decorator

      from typing import Callable, Any
      
      def log_call(func: Callable[..., Any]) -> Callable[..., Any]:
          def wrapper(*args: Any, **kwargs: Any) -> Any:
              print(f"Calling {func.__name__}()...")
              result = func(*args, **kwargs)
              print(f"Finished {func.__name__}()!")
              return result
          return wrapper
      
      @log_call
      def say_hi(name: str) -> None:
          print(f"Hi, {name}!")
      
      say_hi("Alice")
      # Output:
      # Calling say_hi()...
      # Hi, Alice!
      # Finished say_hi()!
      
      @log_call
      def add(a: int, b: int) -> int:
          return a + b
      
      sum_result = add(5, 3)
      print(sum_result)
      # Output:
      # Calling add()...
      # Finished add()!
      # 8
      

      Decorators with Arguments

      If your function takes arguments, the wrapper should accept them too, using *args and **kwargs. (This is already covered and improved in the previous section's example).

      def repeat(func: Callable[..., Any]) -> Callable[..., Any]:
          def wrapper(*args: Any, **kwargs: Any) -> Any:
              func(*args, **kwargs)
              func(*args, **kwargs)
          return wrapper
      
      @repeat
      def greet(name: str) -> None:
          print(f"Hello, {name}!")
      
      greet("Alice")
      # Output:
      # Hello, Alice!
      # Hello, Alice!
      

      Stacking Multiple Decorators

      You can apply more than one decorator to a function. They are applied from the closest to the function outward.

      @decorator_one
      @decorator_two
      def my_func():
          pass
      # This is the same as: my_func = decorator_one(decorator_two(my_func))
      

      Advanced Type Hinting with ParamSpec

      When you use type hints with decorators, especially for functions that take various arguments, the simple Callable[..., Any] can make you lose the specific type information of the original function's arguments. This is where ParamSpec comes in.

      ParamSpec is used to forward the parameter types of one callable to another callable. This helps type checkers understand that the wrapper function in your decorator has the exact same arguments as the original function it wraps.

      Example:

      from typing import Callable, Any, ParamSpec
      
      P = ParamSpec('P') # Define a ParamSpec variable
      T = Any         # Define a type variable for return type (or use TypeVar)
      
      # Now, we can hint the decorator more accurately
      def simple_decorator(func: Callable[P, T]) -> Callable[P, T]:
          # The wrapper now explicitly takes the same parameters (P) and returns the same type (T)
          def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
              print("Something before!")
              result = func(*args, **kwargs)
              print("Something after!")
              return result
          return wrapper
      
      @simple_decorator
      def add_numbers(a: int, b: int) -> int:
          return a + b
      
      @simple_decorator
      def say_word(word: str) -> None:
          print(word)
      
      # Type checkers now know the signatures are preserved:
      sum_result = add_numbers(10, 20) # Type checker knows add_numbers takes two ints and returns int
      print(sum_result) # Output: Something before!
      # 30
      # Something after!
      
      say_word("hello") # Type checker knows say_word takes a string and returns None
      # Output: Something before!
      # hello
      # Something after!
      
      • P = ParamSpec('P') creates a placeholder for parameter specification.
      • Callable[P, T] indicates a function that takes parameters specified by P and returns type T.
      • The wrapper function's signature (*args: P.args, **kwargs: P.kwargs) -> T ensures the type checker knows it matches the decorated function's signature.

      Using ParamSpec provides more precise type hinting for complex decorators, improving static analysis and code clarity.

      Why Use Decorators?

      • Code reuse: Add the same feature to many functions without repeating code.
      • Separation of concerns: Keep extra logic (like logging) out of your main function code.
      • Cleaner code: Make your code easier to read and maintain.

      Common Beginner Mistakes

      • Forgetting to return the wrapper function from the decorator.
      • Not using *args and **kwargs in the wrapper, so the decorator only works for functions with no arguments.
      • Losing the original function's name and docstring (can be fixed with functools.wraps).

      Example of a mistake:

      from typing import Any
      
      def bad_decorator(func: Callable[..., Any]) -> Callable[..., Any]:
          def wrapper(*args: Any, **kwargs: Any) -> Any:
              print("Something!")
          # Forgot to call func() and forgot to return wrapper
          return wrapper # Added return to make it a valid (though simple) decorator
      
      @bad_decorator
      def test_func(x: int) -> None:
          print(f"Original function called with: {x}")
      
      test_func(10)
      # Output:
      # Something!
      # (Original function is not called)
      

      Summary

      • Decorators are functions that add extra behavior to other functions.
      • Use @decorator_name to apply a decorator.
      • Decorators help you reuse code and keep your functions clean.
      • Always use *args and **kwargs in your wrapper if the function takes arguments.

      Continue Learning

      Python Variables

      Popular

      Getting Started: Understanding Variables in Python In programming, we often need to store informatio

      Python Properties

      For You

      Controlling Attribute Access: Understanding Properties In Python, you often want to control how the

      Python Strings

      For You

      Working with Text: Understanding Strings in Python Text is everywhere in the world of programming. W

      Personalized Recommendations

      Log in to get more relevant recommendations based on your reading history.