- Cyber Success
- September 21, 2026
- IT Courses
Python Logging and Exception Handling: A Beginner’s Guide to Debugging Like a Pro
Most Python beginners debug the same way: sprinkle print() statements everywhere, run the code, squint at the output, delete the print statements once the bug is fixed, and repeat the entire process the next time something breaks. Professional Python developers don’t work this way — they build proper logging and deliberate exception handling into their code from the start, which turns debugging from a repeated manual scramble into a systematic, traceable process.
Why print() Statements Aren’t Real Debugging
Logging is an essential part of software development, often overlooked until something goes wrong, and using it effectively requires more than just sprinkling print() statements or basic log calls throughout your code. Print statements have no severity levels, no timestamps, no way to selectively silence noisy output in production while keeping it available for debugging, and no way to route messages to a file for later analysis — Python’s built-in logging module solves all of these problems, and it’s part of the standard library, requiring no installation.
The Five Logging Levels, and When to Actually Use Each
Python’s logging module provides five severity levels, each meant to be used deliberately rather than defaulting everything to a single level out of habit:
Level | When to Use It |
DEBUG | Detailed diagnostic information, useful mainly during active development and troubleshooting |
INFO | Confirmation that things are working as expected — regular application flow, not just errors |
WARNING | Something unexpected happened, or a potential issue is developing, but the application is still working normally |
ERROR | A more serious problem occurred — the application failed to perform a specific function |
CRITICAL | A severe error indicating the program itself may be unable to continue running |
A common beginner mistake is logging everything at the ERROR level regardless of actual severity — reserving ERROR and CRITICAL specifically for true system failures (a database connection loss, disk full), while using WARNING or INFO for predictable, non-fatal application events (a failed login attempt, invalid API input) keeps your logs genuinely useful rather than creating alert fatigue where every log line looks equally urgent.
Basic Logging Setup
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
logger.info(“Application started”)
logger.warning(“Config file not found, using defaults”)
logger.error(“Failed to connect to database”)
Using __name__ as the logger’s name is a specific, recommended best practice — it gives each module its own logger with a name reflecting its location in your project (e.g., myapp.database), making it far easier to trace exactly where a log message originated once your application grows beyond a single file.
The Single Most Important Logging Habit: logger.exception()
When handling exceptions, log the full stack trace using logger.exception() inside your except block — this single habit is what separates genuinely useful debugging logs from ones that only tell you that something failed, without telling you why.
try:
result = 10 / 0
except ZeroDivisionError:
logger.exception(“An error occurred during division”)
logger.exception() is essentially shorthand for calling logger.error() with exc_info=True automatically included — it captures the full traceback, not just your custom error message, which is invaluable for debugging errors that only surface once code is already running in production, far from your development environment.
# These two are functionally equivalent:
logger.exception(“An error occurred during division”)
logger.error(“An error occurred during division”, exc_info=True)
Exception Handling: Writing Deliberate, Not Accidental, Error Handling
Exception handling in Python uses try/except blocks to catch and respond to errors gracefully, rather than letting an unhandled exception crash the entire program. The core mistake beginners make isn’t forgetting to use try/except — it’s using it carelessly, catching exceptions broadly and silently, which hides real problems rather than solving them.
# A common beginner mistake — swallows the error with no trace
try:
value = int(user_input)
except:
pass
# A better approach — specific exception type, logged, with a clear fallback
try:
value = int(user_input)
except ValueError:
logger.warning(f”Invalid input received: {user_input!r}, defaulting to 0″)
value = 0
The second version catches a specific, expected exception type rather than a bare except, logs what actually happened (including the problematic input itself), and defines a clear, intentional fallback — turning a silent failure into a traceable, understandable one.
Three Common Exception-Logging Patterns
- Catch-log-rethrow: Log the exception for visibility, then re-raise it so the calling code (or a higher-level handler) can still respond to the failure — useful when a function shouldn’t swallow an error it doesn’t have enough context to fully resolve.
- Catch-log-return: Log the exception and return a sensible default or fallback value, letting the program continue running rather than crashing outright — appropriate for non-critical failures where a reasonable default exists.
- Catch-log-retry: Log the exception and attempt the operation again, often with a brief delay — commonly used for transient failures like a flaky network request, where the same operation might succeed on a second attempt.
Choosing the right pattern deliberately, based on what actually makes sense for the specific failure, is what separates genuinely robust exception handling from reflexively wrapping everything in a try/except block “just in case.”
Adding Context: Logs That Actually Help You Debug
A log message that just says “Error occurred” tells you almost nothing useful six months later at 2 AM when something breaks in production. Including contextual details — variable values, user IDs, function names, relevant identifiers — significantly aids troubleshooting and debugging by making it clear not just that something failed, but what state the application was in when it did.
logger.error(“Failed to process order %s for user %s”, order_id, user_id)
Using the logging module’s built-in %s placeholder-style formatting (rather than an f-string) inside the log call is a specific best practice worth adopting — the logging module only performs the string formatting if the message is actually going to be processed and output, meaning expensive formatting operations or function calls are deferred and skipped entirely when that log level isn’t active, which f-strings can’t do since they’re evaluated immediately regardless of whether the log will actually be shown.
Logging to a File Instead of Just the Console
Beyond basic console output, directing logs to a file allows you to review application behavior after the fact, particularly useful for catching intermittent or hard-to-reproduce bugs.
import logging
logging.basicConfig(
filename=”app.log”,
level=logging.INFO,
format=”%(asctime)s – %(name)s – %(levelname)s – %(message)s”
)
This configuration adds a timestamp, the logger’s name, and the severity level to every log entry — a consistent, structured format that makes logs meaningfully easier to scan, filter, and analyze compared to plain, unstructured print output.
A Beginner-to-Pro Debugging Checklist
- Replace print() with logging as your default debugging tool, even in small personal projects — building the habit early pays off significantly once projects grow.
- Use logger = logging.getLogger(__name__) in every module rather than logging through the root logger directly, so you can trace exactly which part of your application generated a given message.
- Choose specific exception types in your except blocks (ValueError, KeyError, FileNotFoundError) rather than a bare except, so you only catch the failures you actually anticipated.
- Always use exception() inside except blocks where you want the full traceback captured — this is the single highest-leverage habit for making future debugging easier.
- Add meaningful context to every log message — relevant variable values or identifiers — rather than generic messages like “Error occurred” that provide no useful trail to follow.
Final Word
Moving from print()-based debugging to proper logging and deliberate exception handling is one of the clearest markers of a beginner becoming a genuinely professional Python developer — it’s not about writing more code, but about writing code that tells you clearly what happened when something eventually breaks, which it inevitably will. Building the habit of using logger.exception(), choosing specific exception types, and adding real context to log messages pays off enormously the first time you’re debugging a production issue you can’t easily reproduce.
Cyber Success’s Python courses in Pune build proper logging and exception handling into project-based training from the start, helping you develop debugging habits that scale well beyond classroom exercises. Explore our Python course programs to build genuinely professional Python development habits.
Frequently Asked Questions
What’s the difference between logger.error() and logger.exception() in Python?
Logger.exception() is essentially shorthand for logger.error() called with exc_info=True — it automatically captures and includes the full stack trace in the log output, and should specifically be used inside an except block, since calling it outside exception handling context won’t have a meaningful traceback to include.
Why shouldn’t I just use print() statements for debugging?
Print statements lack severity levels, timestamps, and the ability to selectively route or silence output — Python’s logging module solves all of these, letting you keep detailed debugging information available without it cluttering production output, and letting you direct logs to a file for later review.
Is it bad practice to use a bare except: with no specific exception type?
Yes, generally — a bare except block catches every possible exception, including ones you didn’t anticipate and may not want to silently swallow, which can hide genuine bugs; catching specific exception types (ValueError, KeyError) lets you handle expected failures deliberately while allowing unexpected ones to surface visibly.
Should I log every single function call in my Python program?
No — logging everything at the same level creates noisy, hard-to-use logs. Reserve DEBUG for detailed diagnostic information mainly useful during active development, INFO for confirming normal application flow, and ERROR/CRITICAL specifically for genuine failures, so your logs stay meaningfully filterable by severity.
What are the three main exception-logging patterns, and when should I use each?
Catch-log-rethrow (log for visibility, then let a higher-level handler respond) suits errors you can’t fully resolve locally; catch-log-return (log and provide a sensible default) suits non-critical failures with a reasonable fallback; catch-log-retry (log and attempt the operation again) suits transient failures like flaky network requests.
