We’ve all been there: you’re writing Python code, something isn’t behaving as expected, and your first instinct is to scatter a few print()
statements around. A quick print("Reached here")
to see if the function runs, or print("x:", x)
to check a variable’s value. It usually does the job, at least, until it doesn’t.
That was also my approach. print()
was my sidekick, always there to give me a quick peek under the hood.
But here’s the thing: print()
is fine when you’re tinkering, not when you’re building. As your code grows up, you need more than quick checks, you need context, structure, and persistence.
That’s where Python’s logging
module comes in. Logging isn’t just a fancier print()
; it’s a complete toolkit that transforms debugging. It tells you what your program is doing, when it’s doing it, and - most importantly - why things go wrong.
Why print()
Isn’t Enough
At first, print()
seems really usefull. Drop a line in your code, run it, and voilà, you see what’s happening. But as soon as your project grows beyond a small script, the cracks start to show.
Here’s where print()
falls short:
- No levels of importance: Every message looks identical. Whether it’s a harmless check or a serious error,
print("something went wrong")
doesn’t tell you how worried you should be. - No timestamps or context: When did the error happen? In which part of the code?
print()
won’t say. All you get is raw text, which quickly becomes useless once things get complicated. - No persistence: The moment your program finishes, your precious debug trail disappears. If something breaks overnight, you wake up with zero evidence of what happened.
- Hard to clean up: Nothing clutters a codebase faster than dozens of stray
print()
statements. Commenting them out, deleting them, or forgetting one in production is a headache we’ve all endured. - Not scalable:
print()
might be fine when you’re coding alone, but in production systems or team projects, it simply doesn’t cut it. Logs are a shared language;print()
is just noise.
print()
is like a flashlight, it helps in a dark room, but it won’t light up the whole path when you’re navigating a forest. For that, you need something built for the job: logging.
What Logging Brings to the Table
Switching from print()
to logging, it's like stepping out of the dark and into a control room. Suddenly, I wasn’t just seeing raw messages, I was getting organized, meaningful information.
Here’s why logging makes such a difference:
- Log levels: Not every message deserves the same attention. With logging, I can mark something as
DEBUG
when I’m tracing the flow, useINFO
for normal events, raise aWARNING
for potential issues, and reserveERROR
andCRITICAL
for real problems. It’s like going from flat text to a prioritized alert system. - Configurability: Tired of drowning in debug messages? With logging, I don’t have to delete anything, I just change the log level. One setting controls how much detail I see, whether I want everything during development or only warnings in production.
- Context: Logs aren’t just messages; they come with timestamps, module names, even line numbers. That means when something breaks, I don’t just see what happened, I know when and where.
- Persistence: Unlike
print()
, logs can stick around. They can be written to files, rotated daily, or shipped off to monitoring tools. If my script crashes at 2 AM, I wake up to a breadcrumb trail instead of an empty console. - Professionalism: At the end of the day, logging is the standard. Teams, frameworks, and production systems rely on it because it scales. It’s the difference between duct tape debugging and engineering with discipline.
Logging doesn’t just make debugging easier, it gives your code a voice, one that can whisper the little details or sound the alarm when things go wrong.
Getting Started with Logging
The good news? Making the jump from print()
to logging is surprisingly easy. You don’t need to overhaul your whole project, you can start small with just a few lines of setup.
Here’s the simplest way to get going:
import logging
logging.basicConfig(level=logging.DEBUG)
logging.debug("This is a debug message")
logging.info("The application is running smoothly")
logging.warning("Something looks off, but still running")
logging.error("Something went wrong")
logging.critical("We’re on fire!")
Run that, and you’ll see output with log levels automatically attached, something like:
DEBUG:root:This is a debug message
INFO:root:The application is running smoothly
WARNING:root:Something looks off, but still running
ERROR:root:Something went wrong
CRITICAL:root:We’re on fire!
Notice how much more context you get compared to plain print()
statements.
With print()
, all you see is the text. With logging
, you instantly know it’s an error, not just another random message. And with one configuration tweak, you could include timestamps, file names, and even line numbers.
It’s a tiny shift in how you write your code, but it pays off the first time you have to trace an issue.
Customizing Logs
The real power comes from customization. By tweaking just a few settings, I could turn plain log messages into something far more useful.
Changing Formats
Want to know not just what happened, but also when and where? That’s as simple as adjusting the format:
import logging
logging.basicConfig(
level=logging.DEBUG,
format="%(asctime)s - %(levelname)s - %(filename)s:%(lineno)d - %(message)s"
)
logging.warning("Something might be off here")
Now every log tells me the timestamp, severity, file, and line number:
2025-09-18 08:34:06,157 - WARNING - example03.py:8 - Something might be off here
Suddenly, my logs feel like a black box recorder for my code.
Writing Logs to a File
Printing to the console is fine for quick debugging, but what if your program runs overnight or on a server? You’ll want logs written to a file:
This article is for subscribers only
To continue reading this article, just register your email and we will send you access.
Subscribe NowAlready have an account? Sign In