Python Decorators: Understanding @property, @classmethod, and @staticmethod
Confused by Python decorators? This guide demystifies @property, @classmethod, and @staticmethod, showing you how to write cleaner, modular code.

Have you ever seen @property, @staticmethod and felt that look of confusion? Before understanding these three, we need to understand what a decorator is for real.
First: functions are first-class citizens
In Python, functions are objects like any other. This means you can pass functions as arguments, return functions and store functions in variables.
def saudacao(): print("Olá, mundo!")# Guardando a função numa variável (sem os parênteses!)minha_fn = saudacaominha_fn() # Olá, mundo!# Passando função como argumentodef executar(fn): print("Antes...") fn() print("Depois.")executar(saudacao)# Antes...# Olá, mundo!# Depois.💡 Why does this matter? A decorator is nothing more than a function that receives another function, adds some behavior, and returns a new function. That is exactly it.
Building a decorator from scratch
Let's create a decorator that measures the execution time of any function. Useful for debugging and optimization.
import timedef medir_tempo(funcao): # funcao é a função que vamos "decorar" def wrapper(*args, **kwargs): inicio = time.time() resultado = funcao(*args, **kwargs) # executa a função original fim = time.time() print(f"⏱️ {funcao.__name__} levou {fim - inicio:.4f}s") return resultado return wrapper # retorna a função "embrulhada"# Sem a sintaxe @, seria assim:def processar_dados(): time.sleep(0.5) print("Dados processados!")processar_dados = medir_tempo(processar_dados) # manual e feioprocessar_dados()🧠 Mental model: the decorator is a wrapper. The original function goes in, the decorator puts it inside a box with extra behaviors, and returns the box.
Syntactic sugar: the @
The @ is just a prettier way to write exactly what we did above. Both versions are identical:
# Com @ — a forma que você vai usar sempre@medir_tempodef processar_dados(): time.sleep(0.5) print("Dados processados!")processar_dados()# Dados processados!# ⏱️ processar_dados levou 0.5003s# Funciona em qualquer função!@medir_tempodef buscar_usuario(id): time.sleep(0.1) return {"id": id, "nome": "Genildo"}usuario = buscar_usuario(42)# ⏱️ buscar_usuario levou 0.1001s✨ Got the magic? You wrote the logic to measure time only once and applied it to any function with a
@. This is the DRY (Don't Repeat Yourself) principle in action.
Now for real: OOP decorators
Python has three native decorators for classes that you will see everywhere:
Allows methods to be accessed like attributes for controlled access.
Defines methods bound to the class, not instances.
Creates independent functions inside the class without access to instance or class.
Key decorators that shape class behavior and usage
Decorator | What it does |
|---|---|
| Method that looks like an attribute |
| Class method, not object method |
| Independent function inside the class |
@property — controlled access with elegance
Remember encapsulation with __saldo in Part 1? The @property allows you to access protected data as if they were simple attributes, but with logic underneath.
class Produto: def __init__(self, nome, preco): self.nome = nome self.__preco = preco # privado @property def preco(self): # getter: chamado ao ler produto.preco return f"R$ {self.__preco:.2f}" @preco.setter def preco(self, valor): # setter: chamado ao atribuir produto.preco = X if valor < 0: raise ValueError("Preço não pode ser negativo! 🚫") self.__preco = valorcamiseta = Produto("Camiseta Blueprint", 89.90)print(camiseta.preco) # R$ 89.90 — parece atributo, mas é método!camiseta.preco = 79.90 # usa o setter automaticamenteprint(camiseta.preco) # R$ 79.90camiseta.preco = -10 # ValueError: Preço não pode ser negativo! 🚫🔶 When to use @property? When you want access to data to have built-in logic — formatting, validation, derived calculation — but the external API remains clean, without
get_preco()andset_preco().
@classmethod — building objects in alternative ways
A @classmethod receives the class as the first argument (conventionally cls), not the object. It is widely used to create factory methods — alternative ways to instantiate a class.
class Artigo: def __init__(self, titulo, autor, categoria): self.titulo = titulo self.autor = autor self.categoria = categoria @classmethod def de_dicionario(cls, dados: dict): # cria um Artigo a partir de um dict (ex: resposta de API) return cls( titulo=dados["title"], autor=dados["author"], categoria=dados["category"] ) @classmethod def rascunho(cls, titulo): # cria um artigo rascunho com defaults return cls(titulo=titulo, autor="Desconhecido", categoria="Rascunho") def __repr__(self): return f"[{self.categoria}] {self.titulo} — {self.autor}"# Forma normala1 = Artigo("OOP em Python", "Genildo", "Python")# Via dicionário (vindo de uma API, por exemplo)payload = {"title": "Decorators", "author": "Genildo", "category": "Python"}a2 = Artigo.de_dicionario(payload)# Rascunho rápidoa3 = Artigo.rascunho("Ideia de artigo novo")print(a2) # [Python] Decorators — Genildoprint(a3) # [Rascunho] Ideia de artigo novo — Desconhecido@staticmethod — stateless utility
A @staticmethod does not receive self or cls. It is a common function that lives inside the class for logical organization, not for coupling.
class ValidadorSenha: @staticmethod def tem_tamanho_minimo(senha: str) -> bool: return len(senha) >= 8 @staticmethod def tem_numero(senha: str) -> bool: return any(c.isdigit() for c in senha) @staticmethod def validar(senha: str) -> bool: return ( ValidadorSenha.tem_tamanho_minimo(senha) and ValidadorSenha.tem_numero(senha) )print(ValidadorSenha.validar("abc")) # False — curta e sem númeroprint(ValidadorSenha.validar("blueprint1")) # True ✅⚠️ Quick rule to avoid confusion:
Need to access
self(object data)? → normal methodNeed to access
cls(the class itself)? →@classmethodDon't need either? →
@staticmethod
Custom decorator + OOP together
To wrap it up: a decorator that automatically logs every method call in a class. This pattern appears in real-world frameworks.
def logar_chamada(funcao): def wrapper(*args, **kwargs): print(f"📋 Chamando: {funcao.__name__}()") resultado = funcao(*args, **kwargs) print(f"✅ Concluído: {funcao.__name__}()") return resultado return wrapperclass ServicoDePost: def __init__(self): self.__posts = [] @logar_chamada def criar_post(self, titulo): self.__posts.append(titulo) return titulo @logar_chamada def publicar_todos(self): for post in self.__posts: print(f" 🚀 Publicando: {post}") @property def total(self): return len(self.__posts) @staticmethod def slug(titulo: str) -> str: return titulo.lower().replace(" ", "-")servico = ServicoDePost()servico.criar_post("OOP em Python")servico.criar_post("Decorators em Python")servico.publicar_todos()print(f"Total: {servico.total}")print(ServicoDePost.slug("Decorators em Python"))# decorators-em-pythonWhat you learned today
✅ Functions are objects in Python — they can be passed and returned.
✅ A decorator is a function that wraps another function.
✅ The
@is syntactic sugar — no magic, just elegance.✅
@propertycreates controlled access to private attributes.✅
@classmethodoperates on the class, not the object — great for factory methods.✅
@staticmethodis a utility function that lives in the class for organization.✅ Custom decorators + OOP = clean and reusable code.


