Last modified: Nov 02, 2025 By Alexander Williams
Python Typer Logging vs Echo Strategy
Python Typer offers two main output methods. You can use typer.echo or Python logging. Each approach has specific use cases.
Understanding when to use each method improves CLI application quality. This guide explores both strategies in depth.
Understanding Typer Echo Function
typer.echo is Typer's built-in output function. It works like Python's print but handles edge cases better.
It automatically handles encoding issues. It also works well with different output streams.
Here's basic echo usage:
import typer
app = typer.Typer()
@app.command()
def hello(name: str):
typer.echo(f"Hello {name}!")
# Simple output for user feedback
if __name__ == "__main__":
app()
$ python app.py John
Hello John!
Implementing Verbose and Quiet Modes
Verbose and quiet modes control output detail. They help users get the right information level.
Use boolean flags for these modes. Typer makes this easy with option decorators.
import typer
app = typer.Typer()
@app.command()
def process_data(
input_file: str,
verbose: bool = typer.Option(False, "--verbose", "-v"),
quiet: bool = typer.Option(False, "--quiet", "-q")
):
if verbose and quiet:
typer.echo("Cannot use both verbose and quiet modes")
raise typer.Exit(1)
if not quiet:
typer.echo(f"Processing {input_file}...")
# Simulate processing
if verbose:
typer.echo("Step 1: Reading file...")
typer.echo("Step 2: Processing data...")
typer.echo("Step 3: Writing output...")
if not quiet:
typer.echo("Processing complete!")
if __name__ == "__main__":
app()
$ python app.py data.txt --verbose
Processing data.txt...
Step 1: Reading file...
Step 2: Processing data...
Step 3: Writing output...
Processing complete!
$ python app.py data.txt --quiet
# No output except errors
Python Logging Integration
Python logging provides structured, configurable output. It's better for complex applications.
Logging supports different levels. These include DEBUG, INFO, WARNING, ERROR, and CRITICAL.
Here's logging integration with Typer:
import typer
import logging
import sys
# Configure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s',
handlers=[logging.StreamHandler(sys.stderr)]
)
app = typer.Typer()
logger = logging.getLogger(__name__)
@app.command()
def analyze_data(
input_path: str,
log_level: str = typer.Option("INFO", "--log-level")
):
# Set log level based on user input
logger.setLevel(getattr(logging, log_level.upper()))
logger.debug(f"Starting analysis of {input_path}")
logger.info(f"Processing file: {input_path}")
# Simulate analysis
try:
logger.debug("Reading file contents")
# File processing logic
logger.info("Analysis completed successfully")
except Exception as e:
logger.error(f"Analysis failed: {str(e)}")
raise typer.Exit(1)
if __name__ == "__main__":
app()
$ python app.py data.csv --log-level DEBUG
2023-10-15 10:30:45 - DEBUG - Starting analysis of data.csv
2023-10-15 10:30:45 - INFO - Processing file: data.csv
2023-10-15 10:30:45 - DEBUG - Reading file contents
2023-10-15 10:30:45 - INFO - Analysis completed successfully
Combining Echo and Logging
Many applications benefit from both approaches. Use echo for user-facing messages.
Use logging for debugging and operational details. This separation keeps interfaces clean.
Here's a combined approach:
import typer
import logging
from typing import Optional
app = typer.Typer()
logger = logging.getLogger(__name__)
def setup_logging(verbose: bool, quiet: bool):
"""Configure logging based on verbosity"""
level = logging.DEBUG if verbose else logging.WARNING if quiet else logging.INFO
logging.basicConfig(level=level, format='%(levelname)s: %(message)s')
@app.command()
def deploy_app(
environment: str,
verbose: bool = typer.Option(False, "--verbose"),
quiet: bool = typer.Option(False, "--quiet")
):
setup_logging(verbose, quiet)
if not quiet:
typer.echo(f"🚀 Deploying to {environment} environment")
logger.debug("Starting deployment process")
# Deployment steps
if not quiet:
typer.echo("✓ Checking dependencies...")
logger.debug("Verifying system requirements")
if not quiet:
typer.echo("✓ Uploading application...")
logger.info(f"Uploading to {environment}")
if verbose:
typer.echo("Detailed deployment log available in deploy.log")
if not quiet:
typer.echo("✅ Deployment completed successfully")
if __name__ == "__main__":
app()
$ python app.py production
🚀 Deploying to production environment
✓ Checking dependencies...
✓ Uploading application...
✅ Deployment completed successfully
$ python app.py staging --verbose
🚀 Deploying to staging environment
✓ Checking dependencies...
✓ Uploading application...
Detailed deployment log available in deploy.log
✅ Deployment completed successfully
Structured Logging with JSON
Structured logging outputs machine-readable format. JSON is popular for this purpose.
It helps with log analysis tools. It also provides better search capabilities.
Here's JSON logging implementation:
import typer
import logging
import json
from datetime import datetime
class JSONFormatter(logging.Formatter):
def format(self, record):
log_entry = {
"timestamp": datetime.utcnow().isoformat(),
"level": record.levelname,
"message": record.getMessage(),
"module": record.module,
"function": record.funcName
}
return json.dumps(log_entry)
app = typer.Typer()
@app.command()
def api_call(
endpoint: str,
structured: bool = typer.Option(False, "--structured-logs")
):
logger = logging.getLogger()
handler = logging.StreamHandler()
if structured:
handler.setFormatter(JSONFormatter())
else:
handler.setFormatter(logging.Formatter('%(levelname)s: %(message)s'))
logger.addHandler(handler)
logger.setLevel(logging.INFO)
logger.info(f"Calling API endpoint: {endpoint}")
# Simulate API call
logger.info("API response received")
logger.error("Rate limit approaching") if endpoint == "users" else None
if __name__ == "__main__":
app()
$ python app.py users --structured-logs
{"timestamp": "2023-10-15T10:35:22", "level": "INFO", "message": "Calling API endpoint: users", "module": "app", "function": "api_call"}
{"timestamp": "2023-10-15T10:35:22", "level": "INFO", "message": "API response received", "module": "app", "function": "api_call"}
{"timestamp": "2023-10-15T10:35:22", "level": "ERROR", "message": "Rate limit approaching", "module": "app", "function": "api_call"}
Best Practices and Performance
Choose echo for simple user interaction. Use logging for complex applications.
Consider your audience. End users prefer clear echo messages. Developers need detailed logs.
For performance, avoid expensive operations in echo calls. Use lazy evaluation when possible.
Our Python Typer performance guide covers optimization techniques.
Error Handling and Exit Codes
Both echo and logging handle errors differently. Echo works well with Typer's error system.
Logging provides better error context. Combine them for comprehensive error reporting.
import typer
import logging
app = typer.Typer()
logger = logging.getLogger(__name__)
@app.command()
def critical_operation():
try:
# Simulate operation
result = 10 / 0
typer.echo(f"Result: {result}")
except Exception as e:
logger.error(f"Operation failed: {str(e)}", exc_info=True)
typer.echo("❌ Operation failed. Check logs for details.")
raise typer.Exit(1)
if __name__ == "__main__":
app()
For advanced error handling, see our Typer global state guide.
Integration with Other Typer Features
Typer logging works well with other advanced features. Context objects help manage application state.
Global options can control logging behavior across commands. Callbacks validate and setup logging configuration.
Learn more in our global options guide.
Conclusion
Choose typer.echo for simple user communication. It's perfect for progress updates and results.
Use Python logging for operational details and debugging. It offers better structure and filtering.
Combine both approaches for professional applications. Echo for users, logging for developers.
Consider your application's complexity and audience. Simple tools need echo. Complex systems need logging.
Both strategies make Typer applications more robust and user-friendly when used appropriately.