What Does the @ Symbol Do in Python

The @ symbol in Python does two completely different things depending on where it appears. When placed on a line above a function or class definition, it applies a decorator. When placed between two values in an expression, it performs matrix multiplication. These are unrelated features that share a symbol, which is why the @ character can be confusing the first time you encounter it. This article explains both uses from scratch, with code examples that demonstrate exactly what Python does when it sees @.

Python introduced the @ symbol for decorators in version 2.4, following the proposal in PEP 318. Over a decade later, Python 3.5 added a second use for @ as the matrix multiplication operator, following PEP 465. The two uses are distinguished entirely by context: Python's parser knows which meaning to apply based on where the symbol appears in your code.

Use 1: Decorator Syntax

The primary use of @ in Python is decorator syntax. When you see @something on the line directly above a def or class statement, Python is applying a decorator to that function or class. A decorator is a function that takes another function as its argument and returns a modified version of it.

def shout(func):
    def wrapper(*args, **kwargs):
        result = func(*args, **kwargs)
        return result.upper()
    return wrapper


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


print(greet("Kandi"))
# HELLO, KANDI

The @shout line tells Python to pass the greet function to the shout function and replace greet with whatever shout returns. In this case, shout returns a wrapper function that calls the original greet, takes its return value, and converts it to uppercase. After the decorator is applied, calling greet("Kandi") calls the wrapper function, which in turn calls the original greet and transforms the result.

Without the @ syntax, you would need to write the decoration manually after the function definition. The @ line is a shortcut that keeps the decoration visible at the top of the function, right where you declare it.

How Decorator @ Translates to Code

The @decorator syntax is syntactic sugar. It does not introduce any new behavior that was not already possible in Python. Every use of @ as a decorator can be rewritten as a manual function call. Understanding the translation helps demystify what @ does.

# WITH @ syntax:
@shout
def greet(name):
    return f"hello, {name}"


# WITHOUT @ syntax (identical behavior):
def greet(name):
    return f"hello, {name}"

greet = shout(greet)

These two forms are exactly equivalent. Python's interpreter transforms the first form into the second form before running it. The @shout line above the def is just a cleaner way to write greet = shout(greet) on the line below it.

When a decorator takes arguments, the translation has one more step. The decorator expression is called first to produce the decorator, and then the decorator is called with the function:

def repeat(num_times):
    def decorator(func):
        def wrapper(*args, **kwargs):
            for _ in range(num_times):
                result = func(*args, **kwargs)
            return result
        return wrapper
    return decorator


# WITH @ syntax:
@repeat(num_times=3)
def say_hi():
    print("Hi!")


# WITHOUT @ syntax (identical behavior):
def say_hi():
    print("Hi!")

say_hi = repeat(num_times=3)(say_hi)


say_hi()
# Hi!
# Hi!
# Hi!

@repeat(num_times=3) first calls repeat(num_times=3), which returns the decorator function. Python then calls decorator(say_hi), which returns the wrapper function. The name say_hi is then rebound to that wrapper.

Note

The @ syntax only works on the line immediately above a def or class statement. You cannot use @ to decorate a variable assignment, a lambda, or any other construct. It is exclusively for function and class definitions.

Stacking Multiple @ Decorators

Multiple @ lines can be stacked on top of each other. Python applies them from bottom to top, meaning the decorator closest to the def wraps the function first:

def bold(func):
    def wrapper(*args, **kwargs):
        return f"<b>{func(*args, **kwargs)}</b>"
    return wrapper


def italic(func):
    def wrapper(*args, **kwargs):
        return f"<i>{func(*args, **kwargs)}</i>"
    return wrapper


@bold
@italic
def say_hello():
    return "hello"


print(say_hello())
# <b><i>hello</i></b>

# Equivalent to:
# say_hello = bold(italic(say_hello))

italic wraps say_hello first, then bold wraps the result. The output shows <b> on the outside and <i> on the inside, confirming that bold is the outermost wrapper.

Built-in Decorators You Will See

Python includes several built-in decorators that you will encounter in standard code. Each one uses the same @ syntax, but they serve very different purposes.

class Circle:
    def __init__(self, radius):
        self._radius = radius

    @property
    def radius(self):
        """Access radius like an attribute instead of a method call."""
        return self._radius

    @radius.setter
    def radius(self, value):
        if value < 0:
            raise ValueError("Radius cannot be negative")
        self._radius = value

    @staticmethod
    def unit_circle():
        """Create a circle with radius 1. No access to self needed."""
        return Circle(1)

    @classmethod
    def from_diameter(cls, diameter):
        """Create a circle from diameter. Receives the class, not an instance."""
        return cls(diameter / 2)


c = Circle(5)
print(c.radius)                    # 5 (accessed like an attribute)
c.radius = 10                      # setter validates the value
print(c.radius)                    # 10

unit = Circle.unit_circle()        # called on the class, not an instance
print(unit.radius)                 # 1

half = Circle.from_diameter(20)    # classmethod receives the class itself
print(half.radius)                 # 10.0

@property turns a method into something that behaves like an attribute. @staticmethod removes the automatic self parameter, making the method callable without an instance. @classmethod replaces self with cls, giving the method access to the class itself rather than a specific instance. All three use the same @ syntax to modify how the method behaves.

Pro Tip

The @dataclass decorator from the dataclasses module is another built-in you will see frequently. It automatically generates __init__, __repr__, and __eq__ methods for a class based on its annotated fields, saving significant boilerplate code.

Use 2: Matrix Multiplication Operator

Starting in Python 3.5, the @ symbol gained a second meaning: a binary operator for matrix multiplication. When @ appears between two values in an expression (rather than above a def statement), Python treats it as an operator that calls the __matmul__ method on the left operand.

This use was introduced by PEP 465 specifically to improve readability for scientific and mathematical code. The mnemonic from the PEP is: @ is * for mATrices.

import numpy as np

A = np.array([[1, 2],
              [3, 4]])

B = np.array([[5, 6],
              [7, 8]])

# Matrix multiplication using @
C = A @ B
print(C)
# [[19 22]
#  [43 50]]

# This is identical to:
C_alt = np.matmul(A, B)
print(C_alt)
# [[19 22]
#  [43 50]]

A @ B performs matrix multiplication: each element in the result is the dot product of a row from A and a column from B. This is different from A * B, which performs element-wise multiplication (each element in A is multiplied by the corresponding element in B).

The @ operator significantly improves readability when mathematical formulas involve multiple matrix products. Consider the difference when translating a linear algebra formula into code:

import numpy as np

# Simulated data for linear regression
np.random.seed(42)
X = np.random.rand(100, 3)
y = np.random.rand(100)

# Without @: nested function calls are hard to read
beta_old = np.dot(np.linalg.inv(np.dot(X.T, X)), np.dot(X.T, y))

# With @: reads like the math formula  beta = (X^T X)^-1 X^T y
beta_new = np.linalg.inv(X.T @ X) @ X.T @ y

# Both produce the same result
print(np.allclose(beta_old, beta_new))  # True

The version with @ reads left to right and closely mirrors the mathematical notation. The version with np.dot() nests function calls inside each other, making it harder to trace the order of operations.

Implementing @ on Custom Classes

The @ operator is not limited to NumPy arrays. Any class can support it by implementing the __matmul__ dunder method. Python also recognizes __rmatmul__ (right-hand matrix multiplication) and __imatmul__ (in-place @=):

class Vector:
    def __init__(self, components):
        self.components = list(components)

    def __matmul__(self, other):
        """Dot product via the @ operator."""
        if len(self.components) != len(other.components):
            raise ValueError("Vectors must have the same length")
        return sum(
            a * b for a, b in zip(self.components, other.components)
        )

    def __repr__(self):
        return f"Vector({self.components})"


v1 = Vector([1, 2, 3])
v2 = Vector([4, 5, 6])

# @ calls __matmul__, computing the dot product
dot_product = v1 @ v2
print(dot_product)  # 32  (1*4 + 2*5 + 3*6)

The @ operator is a general-purpose hook. While PEP 465 designed it with matrix multiplication in mind, the __matmul__ method can be given any behavior that makes sense for the class.

How Python Decides Which @ to Use

Python's parser determines the meaning of @ based on its syntactic position. There is no ambiguity, because the two uses appear in different grammatical contexts.

Context Meaning Introduced PEP
@expr on a line above def or class Decorator: func = expr(func) Python 2.4 (2004) PEP 318
A @ B in an expression Matrix multiplication: A.__matmul__(B) Python 3.5 (2015) PEP 465
A @= B as a statement In-place matrix multiplication: A.__imatmul__(B) Python 3.5 (2015) PEP 465

The parser never confuses the two because they occupy different positions in the grammar. A decorator @ always appears at the start of a line, immediately followed by an expression and then a newline. An operator @ always appears between two expressions in the middle of a statement. There is no scenario where the same @ token could be interpreted as both.

Here is a snippet that uses both meanings in the same file to illustrate the distinction:

import numpy as np
from functools import wraps


# @ as a decorator: wraps a function with timing logic
def timer(func):
    import time
    @wraps(func)
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = func(*args, **kwargs)
        elapsed = time.perf_counter() - start
        print(f"{func.__name__}: {elapsed:.4f}s")
        return result
    return wrapper


@timer  # <-- @ means "apply this decorator"
def multiply_matrices(a, b):
    return a @ b  # <-- @ means "matrix multiplication"


A = np.random.rand(500, 500)
B = np.random.rand(500, 500)

C = multiply_matrices(A, B)
# multiply_matrices: 0.0052s
print(C.shape)  # (500, 500)

Line 15 uses @timer as decorator syntax: it passes multiply_matrices to timer and replaces it with the wrapper. Line 17 uses a @ b as the matrix multiplication operator: it calls NumPy's __matmul__ to compute the product of the two arrays. The same symbol, two different meanings, determined entirely by position.

Key Takeaways

  1. The @ symbol has two uses in Python. Above a def or class, it is decorator syntax. Between two values, it is the matrix multiplication operator. These are completely separate features that share a symbol.
  2. Decorator @ is syntactic sugar. Writing @decorator above a function is equivalent to writing func = decorator(func) after the function definition. It does not add any capability that did not already exist.
  3. Matrix multiplication @ calls __matmul__. Introduced in Python 3.5 via PEP 465, the operator improves readability for scientific code by allowing A @ B instead of np.matmul(A, B) or np.dot(A, B).
  4. Python's parser resolves the ambiguity. A decorator @ appears at the start of a line above a def statement. An operator @ appears between two values inside an expression. There is no case where the meaning is unclear.
  5. Built-in decorators like @property, @staticmethod, and @classmethod use the same @ syntax. They are functions that Python applies to the method definition at the time the class is created, modifying how the method behaves when accessed or called.

The @ symbol is one of the few characters in Python that carries more than one meaning, but the two uses never collide. If you see @ above a function, you are looking at a decorator. If you see @ between two variables, you are looking at matrix multiplication. Recognizing which context you are in is all it takes to read @ correctly.