Formato y estilo de código
Domina el formato de código, la consistencia, PEP 8, las herramientas automatizadas y las técnicas de refactorización
El formato de código es como la puntuación en la escritura: hace que el significado quede claro. Un estilo consistente en toda la base de código reduce la carga cognitiva, evita debates triviales de estilo en las revisiones de código y ayuda a detectar errores con más rapidez.
"El formato de código trata de comunicación, no de preferencia personal. Un estilo consistente en un equipo elimina una clase entera de discusiones sin sentido.": Adaptado de Clean Code
Por qué importa el formato
# Poor formatting: hard to scan and understand
def calculate(items):
result=0
for i in items:
if i['active']==True:
result=result+i['value']
if result>100:
result=result*0.9
return result
# Consistent formatting: easy to read and maintain
def calculate_discounted_total(items: list[dict]) -> float:
result = 0.0
for item in items:
if item["active"]:
result += item["value"]
if result > DISCOUNT_THRESHOLD:
result *= DISCOUNT_RATE
return resultPEP 8: la guía de estilo de Python
PEP 8 es la guía de estilo oficial del código Python. La mayoría de los proyectos de Python la siguen.
Indentación
Usa 4 espacios por nivel de indentación. Nunca mezcles tabuladores y espacios.
# Correct
def fetch_data(url: str) -> dict:
response = requests.get(url)
if response.status_code == 200:
return response.json()
return {}
# Incorrect (2 spaces)
def fetch_data(url: str) -> dict:
response = requests.get(url)
if response.status_code == 200:
return response.json()
return {}Longitud de línea
La longitud máxima de línea es de 79 caracteres para el código y 72 para comentarios y docstrings.
# Too long
result = some_function_with_many_parameters(param_one, param_two, param_three, param_four, param_five)
# Properly wrapped
result = some_function_with_many_parameters(
param_one, param_two, param_three,
param_four, param_five,
)
# Using parentheses for implicit continuation
total = (
subtotal
+ tax_amount
- discount
+ shipping_cost
)Líneas en blanco
import os
import sys
# Two blank lines between top-level definitions
class OrderProcessor:
pass
class EmailService:
pass
# One blank line between methods
class UserManager:
def __init__(self):
self.users = []
def add_user(self, user):
self.users.append(user)
def remove_user(self, user):
self.users.remove(user)Importaciones
# Standard library imports
import os
import sys
from datetime import datetime
# Third-party imports
import pytest
import requests
from fastapi import FastAPI
# Local application imports
from models.user import User
from services.email import EmailServiceHerramientas automatizadas de formato
Black
Black es un formateador de código opinativo que elimina los debates sobre formato. Formatea el código de forma determinista.
# Format a single file
black main.py
# Format entire project
black .
# Check if files would be reformatted
black --check .
# Show differences
black --diff .# Before Black
def complex_function(first_param,second_param,third_param,
fourth_param):
result=first_param+second_param
if result>100:
result=result*0.9
for item in fourth_param:
if item.get('active'):
result+=item['value']
return result
# After Black
def complex_function(
first_param, second_param, third_param, fourth_param
):
result = first_param + second_param
if result > 100:
result = result * 0.9
for item in fourth_param:
if item.get("active"):
result += item["value"]
return resultisort
isort ordena las importaciones alfabéticamente y las agrupa por tipo.
# Before isort
from services.email import EmailService
import os
from models.user import User
import sys
import requests
from datetime import datetime
# After isort
import os
import sys
from datetime import datetime
import requests
from models.user import User
from services.email import EmailServiceflake8
flake8 comprueba las violaciones de PEP 8 y los errores comunes.
# Basic usage
flake8 src/
# With configuration
flake8 src/ --max-line-length 88 --ignore E203,W503Archivo de configuración
# setup.cfg
[flake8]
max-line-length = 88
extend-ignore = E203, W503
exclude = .git, __pycache__, venv
[isort]
profile = black
line_length = 88
[tool:black]
line-length = 88
target-version = py311
include = '\.pyw?$'# pyproject.toml
[tool.black]
line-length = 88
target-version = ["py311"]
[tool.isort]
profile = "black"
line_length = 88
[tool.flake8]
max-line-length = 88
extend-ignore = ["E203", "W503"]Espaciado de variables y atributos
# Inconsistent spacing
name="Alice"
age = 30
address="123 Main St"
# Consistent spacing (PEP 8)
name = "Alice"
age = 30
address = "123 Main St"Comas finales
# Without trailing comma — adding a new item changes two lines
ITEMS = [
"apple",
"banana",
"cherry" # Need to add comma when adding "date"
]
# With trailing comma — cleaner diffs
ITEMS = [
"apple",
"banana",
"cherry",
]Técnicas de refactorización
Extraer método
# Before
def process_report(data):
# Validate
if not data:
raise ValueError("Empty data")
if "revenue" not in data:
raise ValueError("Missing revenue")
if "expenses" not in data:
raise ValueError("Missing expenses")
# Calculate
revenue = sum(data["revenue"])
expenses = sum(data["expenses"])
profit = revenue - expenses
margin = (profit / revenue * 100) if revenue > 0 else 0
# Format
report = f"""
Revenue: ${revenue:,.2f}
Expenses: ${expenses:,.2f}
Profit: ${profit:,.2f}
Margin: {margin:.1f}%
"""
return report
# After
def validate_report_data(data: dict) -> None:
if not data:
raise ValueError("Empty data")
if "revenue" not in data:
raise ValueError("Missing revenue")
if "expenses" not in data:
raise ValueError("Missing expenses")
def calculate_financials(data: dict) -> dict:
revenue = sum(data["revenue"])
expenses = sum(data["expenses"])
profit = revenue - expenses
margin = (profit / revenue * 100) if revenue > 0 else 0
return {"revenue": revenue, "expenses": expenses, "profit": profit, "margin": margin}
def format_report(financials: dict) -> str:
return f"""
Revenue: ${financials['revenue']:,.2f}
Expenses: ${financials['expenses']:,.2f}
Profit: ${financials['profit']:,.2f}
Margin: {financials['margin']:.1f}%
"""
def process_report(data: dict) -> str:
validate_report_data(data)
financials = calculate_financials(data)
return format_report(financials)Renombrar
# Before
def proc(lst):
r = []
for x in lst:
if x[3] == "active":
r.append(x[0])
return r
# After
def get_active_user_emails(users: list[dict]) -> list[str]:
active_emails = []
for user in users:
if user["status"] == "active":
active_emails.append(user["email"])
return active_emailsSimplificar condicionales
# Before: nested conditionals
def can_access(user, resource):
if user:
if user.is_active:
if user.role == "admin" or user.role == "manager":
return True
if resource.is_public:
return True
if resource.owner_id == user.id:
return True
return False
# After: guard clauses
def can_access(user: User | None, resource: Resource) -> bool:
if not user:
return False
if not user.is_active:
return False
if user.role in ("admin", "manager"):
return True
if resource.is_public:
return True
if resource.owner_id == user.id:
return True
return FalseOrganización del código dentro de un archivo
"""Email notification service for the e-commerce platform.
This module handles sending transactional emails using the
SMTP protocol with support for HTML templates.
"""
import logging
import smtplib
from email.mime.text import MIMEText
from typing import Optional
import jinja2
# Constants
DEFAULT_SMTP_PORT = 587
DEFAULT_TIMEOUT = 30
TEMPLATE_DIR = "templates/emails"
logger = logging.getLogger(__name__)
class EmailSender:
"""Handles SMTP connection and email delivery."""
def __init__(self, host: str, port: int = DEFAULT_SMTP_PORT):
self.host = host
self.port = port
self._connection: Optional[smtplib.SMTP] = None
def connect(self, username: str, password: str) -> None:
self._connection = smtplib.SMTP(self.host, self.port, timeout=DEFAULT_TIMEOUT)
self._connection.starttls()
self._connection.login(username, password)
def send(self, to: str, subject: str, body: str) -> None:
if not self._connection:
raise RuntimeError("Not connected. Call connect() first.")
msg = MIMEText(body, "html")
msg["Subject"] = subject
msg["To"] = to
self._connection.send_message(msg)
def disconnect(self) -> None:
if self._connection:
self._connection.quit()
self._connection = NoneFlujo de refactorización
Hooks de pre-commit
Automatiza las comprobaciones de formato con hooks de pre-commit:
# .pre-commit-config.yaml
repos:
- repo: https://github.com/psf/black
rev: 23.12.0
hooks:
- id: black
- repo: https://github.com/pycqa/isort
rev: 5.13.0
hooks:
- id: isort
- repo: https://github.com/pycqa/flake8
rev: 6.1.0
hooks:
- id: flake8
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.8.0
hooks:
- id: mypy# Install pre-commit hooks
pip install pre-commit
pre-commit install
# Run against all files
pre-commit run --all-filesComparación de reglas de formato
Regla | PEP 8 | Black | flake8 |
|---|---|---|---|
Indentación | 4 espacios | 4 espacios | Comprueba |
Longitud máxima de línea | 79 | 88 (configurable) | 79 por defecto |
Comillas | Se prefieren las simples | Dobles | : |
Comas finales | Opcionales | Las añade en varias líneas | : |
Líneas en blanco | 2 a nivel superior, 1 en métodos | Las exige | Comprueba |
Importaciones | Grupos con líneas en blanco | Agrupa | Comprueba el orden |
Deja de debatir sobre el formato. Usa Black: formatea todo de forma consistente y es lo más cercano que Python tiene a un formateador oficial.
No personalices la configuración de Black (salvo la longitud de línea). Toda la gracia de Black está en ser opinativo y eliminar los debates de formato.
Ejercicios prácticos
-
Formatea un archivo: Ejecuta
blacksobre un archivo Python de tu proyecto. Revisa el diff y entiende qué cambió. -
Ordena las importaciones: Ejecuta
isortsobre un archivo con importaciones desordenadas. Haz commit del resultado. -
Haz lint de tu código: Ejecuta
flake8sobre tu proyecto y corrige todas las advertencias. Registra el número de advertencias antes y después. -
Configura pre-commit: Crea un
.pre-commit-config.yamle instala hooks para black, isort y flake8. -
Extrae un método: Encuentra una función de más de 30 líneas. Extráela en funciones más pequeñas hasta que ninguna supere las 15 líneas.
-
Guard clauses: Encuentra una función con condicionales anidados. Refactórala para usar guard clauses.
-
Espaciado consistente: Revisa tu base de código en busca de espaciado inconsistente alrededor de los operadores. Usa Black para corregirlo.
-
Refactura un módulo: Elige un módulo con una organización deficiente. Reorganízalo siguiendo la estructura estándar de archivo (importaciones, constantes, clases, funciones).