TechLead
Lección 25 de 25
5 min de lectura
Python

Buenas practicas de Python

Aprende estandares de codigo Python, estructura de proyectos, linting, formato y practicas profesionales de desarrollo

PEP 8 y estilo de codigo

PEP 8 es la guia de estilo oficial de Python. Seguirla asegura que tu codigo sea consistente y legible. El desarrollo moderno de Python usa herramientas automatizadas como Ruff (linting y formato), Black (formato) e isort (ordenamiento de imports) para aplicar el estilo automaticamente.

# PEP 8 Naming Conventions
my_variable = "snake_case for variables"
MY_CONSTANT = "UPPER_CASE for constants"

def my_function():
    """snake_case for functions."""
    pass

class MyClass:
    """PascalCase for classes."""

    def my_method(self):
        """snake_case for methods."""
        pass

_private_var = "leading underscore for private"
__name_mangled = "double underscore for name mangling"

# Good formatting
def calculate_total(
    items: list[dict],
    tax_rate: float = 0.08,
    discount: float = 0.0,
) -> float:
    """
    Calculate the total price including tax and discount.

    Args:
        items: List of items with 'price' and 'quantity' keys.
        tax_rate: Tax rate as a decimal (default 8%).
        discount: Discount as a decimal (default 0%).

    Returns:
        The total price after tax and discount.
    """
    subtotal = sum(item["price"] * item["quantity"] for item in items)
    tax = subtotal * tax_rate
    total = (subtotal + tax) * (1 - discount)
    return round(total, 2)

Estructura de proyecto

# Recommended project layout
# myproject/
#   pyproject.toml        # Project config, dependencies, tool settings
#   README.md
#   LICENSE
#   .gitignore
#   .env                  # Environment variables (NOT in git!)
#   .env.example          # Template for .env
#   src/
#     myproject/
#       __init__.py
#       main.py           # Entry point
#       config.py          # Configuration management
#       models/
#         __init__.py
#         user.py
#         product.py
#       services/
#         __init__.py
#         user_service.py
#         email_service.py
#       api/
#         __init__.py
#         routes/
#           __init__.py
#           users.py
#           products.py
#         middleware.py
#         dependencies.py
#       utils/
#         __init__.py
#         helpers.py
#         validators.py
#   tests/
#     __init__.py
#     conftest.py          # Shared fixtures
#     test_user_service.py
#     test_api_users.py
#   scripts/
#     seed_data.py
#     migrate.py

Gestion de configuracion

# config.py - using pydantic-settings
from pydantic_settings import BaseSettings
from functools import lru_cache

class Settings(BaseSettings):
    """Application settings loaded from environment variables."""

    app_name: str = "My Application"
    debug: bool = False
    database_url: str = "sqlite:///./app.db"
    secret_key: str
    api_key: str
    redis_url: str = "redis://localhost:6379"

    # Nested settings
    cors_origins: list[str] = ["http://localhost:3000"]
    max_upload_size: int = 10_000_000  # 10MB

    model_config = {
        "env_file": ".env",
        "env_file_encoding": "utf-8",
    }

@lru_cache
def get_settings() -> Settings:
    return Settings()

# Usage
settings = get_settings()
print(settings.database_url)
print(settings.debug)

# .env file
# DATABASE_URL=postgresql://user:pass@localhost/mydb
# SECRET_KEY=your-secret-key-here
# API_KEY=your-api-key
# DEBUG=true

Linting y formato con Ruff

# pip install ruff

# Lint your code
# ruff check .

# Auto-fix issues
# ruff check --fix .

# Format code (like Black)
# ruff format .

# pyproject.toml configuration
# [tool.ruff]
# target-version = "py312"
# line-length = 100
#
# [tool.ruff.lint]
# select = [
#     "E",    # pycodestyle errors
#     "W",    # pycodestyle warnings
#     "F",    # pyflakes
#     "I",    # isort
#     "N",    # pep8-naming
#     "UP",   # pyupgrade
#     "B",    # flake8-bugbear
#     "SIM",  # flake8-simplify
#     "S",    # flake8-bandit (security)
# ]
# ignore = ["E501"]  # Ignore line length (handled by formatter)
#
# [tool.ruff.lint.per-file-ignores]
# "tests/**/*.py" = ["S101"]  # Allow assert in tests

Logging

import logging

# Configure logging
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
    datefmt="%Y-%m-%d %H:%M:%S",
)

# Create a logger
logger = logging.getLogger(__name__)

# Use appropriate levels
logger.debug("Detailed debugging info")
logger.info("General information")
logger.warning("Something unexpected happened")
logger.error("An error occurred")
logger.critical("System is in a critical state")

# Structured logging with extra data
logger.info("User created", extra={"user_id": 42, "email": "alice@example.com"})

# Exception logging
try:
    result = 10 / 0
except ZeroDivisionError:
    logger.exception("Failed to calculate result")
    # This logs the full traceback automatically

# Production logging config
import logging.config

LOGGING_CONFIG = {
    "version": 1,
    "disable_existing_loggers": False,
    "formatters": {
        "default": {
            "format": "%(asctime)s [%(levelname)s] %(name)s: %(message)s",
        },
        "json": {
            "class": "pythonjsonlogger.jsonlogger.JsonFormatter",
            "format": "%(asctime)s %(levelname)s %(name)s %(message)s",
        },
    },
    "handlers": {
        "console": {
            "class": "logging.StreamHandler",
            "formatter": "default",
        },
        "file": {
            "class": "logging.handlers.RotatingFileHandler",
            "filename": "app.log",
            "maxBytes": 10_000_000,
            "backupCount": 5,
            "formatter": "default",
        },
    },
    "root": {
        "level": "INFO",
        "handlers": ["console", "file"],
    },
}

logging.config.dictConfig(LOGGING_CONFIG)

Antipatrones de Python a evitar

# BAD: Mutable default argument
def bad(items=[]):
    items.append(1)
    return items

# GOOD:
def good(items=None):
    if items is None:
        items = []
    items.append(1)
    return items

# BAD: Bare except
try:
    risky()
except:  # Catches SystemExit, KeyboardInterrupt too!
    pass

# GOOD: Specific exception
try:
    risky()
except ValueError as e:
    logger.error(f"Validation error: {e}")

# BAD: Checking type with ==
if type(x) == int:
    pass

# GOOD: isinstance
if isinstance(x, int):
    pass

# BAD: Manual resource management
f = open("file.txt")
data = f.read()
f.close()  # Might not run if exception occurs!

# GOOD: Context manager
with open("file.txt") as f:
    data = f.read()

# BAD: Using global variables
counter = 0
def increment():
    global counter
    counter += 1

# GOOD: Use a class or pass as argument
class Counter:
    def __init__(self):
        self.value = 0
    def increment(self):
        self.value += 1

# BAD: String concatenation in a loop
result = ""
for word in words:
    result += word + " "

# GOOD: Use join
result = " ".join(words)

Puntos clave

  • Usa Ruff: Linter y formateador moderno y rapido que reemplaza multiples herramientas
  • Sigue PEP 8: Un estilo consistente hace el codigo legible y mantenible
  • Logging adecuado: Usa el modulo logging, no print(), en produccion
  • Variables de entorno: Usa pydantic-settings para configuracion con tipado seguro
  • Evita antipatrones: Aprende las trampas comunes y sus soluciones pythonicas

Continuar Aprendiendo