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:
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:
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:
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:
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_codeKubernetes, 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:
# requirements.txt (pinned)
requests==2.32.3
pydantic==2.10.4
redis==5.2.1Regularly 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
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.
Frequently Asked Questions
What is the most important thing to add before deploying Python to production?
Should I use print() for logging in production?
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.
More in this topic
How to Test Python Code
Learn what testing means in Python, the core ideas behind automated tests, and the built-in and third-party tools that make testing practical.
Write Your First Python Unit Test
Write and run your first Python unit test step by step, using unittest to check a small function and understand the test output.
The Factory Pattern in Python
Learn how to use the factory pattern in Python to encapsulate object creation, making your code more flexible and easier to extend.