Best Practices for Production Python

Learn the essential practices for running Python in production, from logging and error handling to configuration management and deployment safety.

7 min read

Production code runs in an environment you do not fully control. Servers restart, networks fail, disks fill up, and users send unexpected input. Development code that works on your laptop can fail in production for reasons that never appeared locally.

This article covers the practical habits that make Python applications safe to deploy, from the first log line to the last health check. Each section addresses a specific production concern, building on the error handling patterns from Handle Python Errors with Exceptions and the configuration approach from Organize Python Projects.

Use structured logging

The logging module is the standard way to record what your application does. Replace print() calls with logger calls at the appropriate level. Use JSON formatting so log aggregation tools can parse and search your logs:

pythonpython
import logging
import json
import sys
 
class JSONFormatter(logging.Formatter):
    def format(self, record):
        payload = {
            "level": record.levelname,
            "message": record.getMessage(),
            "time": self.formatTime(record),
            "module": record.name,
        }
        return json.dumps(payload)
 
def setup_logging():
    handler = logging.StreamHandler(sys.stdout)
    handler.setFormatter(JSONFormatter())
    root = logging.getLogger()
    root.handlers.clear()
    root.addHandler(handler)
    root.setLevel(logging.INFO)

Log at the right level: DEBUG for troubleshooting, INFO for normal events, WARNING for unexpected but handled conditions, ERROR for failures that need attention. Never log at ERROR for routine events; it desensitizes the team to real errors.

Externalize configuration

Hardcoded values that change between environments should live outside the code. Use environment variables for secrets and a configuration file or environment for everything else. The standard library's os.environ is sufficient for simple cases:

pythonpython
import os
 
DATABASE_URL = os.environ["DATABASE_URL"]
REDIS_URL = os.environ.get("REDIS_URL", "redis://localhost:6379")
LOG_LEVEL = os.environ.get("LOG_LEVEL", "INFO")
DEBUG = os.environ.get("DEBUG", "false").lower() == "true"

Never commit secrets to version control. Use a .env file for local development and add it to .gitignore. In production, set environment variables through your deployment platform's dashboard or secrets manager.

For more complex configuration with validation and type conversion, use Pydantic settings. It reads from environment variables, validates types, and fails fast at startup if required values are missing.

Graceful error handling

Production code must not crash on unexpected input. Wrap top-level entry points in try/except blocks that log the full traceback and return an appropriate error response. Never expose raw tracebacks to users:

pythonpython
import traceback
 
def handle_request(request):
    try:
        return process(request)
    except ValidationError as error:
        logger.warning("Invalid request: %s", error)
        return {"status": "error", "message": str(error)}
    except Exception:
        logger.exception("Unhandled error while processing request")
        return {"status": "error", "message": "Internal server error"}

The logger.exception() method automatically includes the full traceback. This is the single most useful logging call in production: it captures everything you need to debug the issue.

For background tasks and message consumers, add a top-level handler that logs the error and continues processing. A single bad message should not crash the entire worker.

Health checks

A health check endpoint tells your deployment platform whether the application is alive and ready to serve traffic. A simple HTTP endpoint at /health that returns 200 OK is the minimum:

pythonpython
def health_check():
    checks = {
        "database": check_database_connection(),
        "cache": check_redis_connection(),
    }
    all_healthy = all(checks.values())
    status_code = 200 if all_healthy else 503
    return {"status": "healthy" if all_healthy else "unhealthy", "checks": checks}, status_code

Kubernetes, AWS load balancers, and most deployment platforms use health checks to decide whether to route traffic to an instance. Without one, a crashed but still-running process continues receiving requests that all fail.

Dependency management

Pin your dependencies to specific versions. A requirements.txt without version pins can install different versions in production than in development, causing failures that are impossible to reproduce locally.

Use pip freeze to generate a lock file with exact versions, or use a modern tool like uv that manages lock files natively. Commit the lock file to version control so every environment installs identical dependencies:

texttext
# requirements.txt (pinned)
requests==2.32.3
pydantic==2.10.4
redis==5.2.1

Regularly update dependencies for security patches, but test the updates in a staging environment before deploying to production.

Monitoring and alerting

Logs tell you what happened. Metrics tell you how often it happens. Add basic metrics for request count, error rate, and response time. The prometheus_client library provides a simple way to expose metrics that monitoring systems can scrape.

At minimum, know the answers to these questions without SSH-ing into a server: how many requests are we handling, what is the error rate, and how fast are responses. If you cannot answer these from a dashboard, your production monitoring needs attention.

For more on building robust applications, see the capstone article on refactoring a real Python project, which applies all these practices to a complete codebase.

Rune AI

Rune AI

Key Insights

  • Use the logging module with structured JSON output instead of print statements.
  • Externalize configuration through environment variables, never hardcode secrets.
  • Catch exceptions at the top level and log full tracebacks for debugging.
  • Add health check endpoints so your deployment platform can detect failures.
  • Pin dependency versions and use a lock file for reproducible builds.
RunePowered by Rune AI

Frequently Asked Questions

What is the most important thing to add before deploying Python to production?

Proper logging. Without structured logs, you cannot debug production issues, measure performance, or detect errors before users report them. Start with the logging module, use JSON format for machine-readability, and log at appropriate levels.

Should I use print() for logging in production?

No. The logging module gives you log levels, timestamp formatting, output routing to files or services, and thread safety. Print statements are unbuffered in some environments and cannot be filtered by severity. Switch from print to logging before deploying.

Conclusion

Production Python is not fundamentally different from development Python. It is the same language with better guardrails: structured logging, externalized configuration, graceful error recovery, and health checks. Add these practices incrementally, and your application will be safer and easier to operate.