Tips for debugging with print()

If you’re embarrassed at debugging with print()
, please don’t be - it’s perfectly fine!
Many bugs are easily tackled with just a few checks in the right places.
As much as I love using a debugger, I often reach for a print()
statement first.
Here are five tips to get the most out of debugging with print()
.
1. Debug variables with f-strings and =
Often we use a print()
to debug the value of a variable, like:
>>> print("widget_count =", widget_count) widget_count = 9001
On Python 3.8+ we can use an f-string with the =
specifier to achieve the same with less typing.
This specifier prints the variable’s name, “=”, and the repr()
of its value:
>>> print(f"{widget_count=}") widget_count=9001
Less typing for the win!
We aren’t limited to variable names with =
.
We can use any expression:
>>> print(f"{(widget_count / factories)=}") (widget_count / factories)=750.0833333333334
If you prefer spaces around your =
, you can add them in the f-string and they will appear in the output:
>>> print(f"{(widget_count / factories) = }") (widget_count / factories) = 750.0833333333334
Neat!
2. Make output “pop” with emoji
Make your debug statements stand out among other output with emoji:
You can also then jump to your debug output with your terminal’s “find” function.
Here are some good emojis for debugging, which may also help express associated emotions:
- 👉 “Reached this point”
- ❌ “Failed as expected”
- ✅ “Worked randomly”
- 🥲 “Trying hard”
- 🤡 “I feel like a software clown”
- 🤯 “WTF”
To type emoji faster, use the keyboard shortcut for your OS:
- Windows: Windows Key + .
- macOS: Control + Command + Space
- Ubuntu: Control + .
- Other Linuxes: 🤷♂️ consult documentation
3. Use rich
or pprint
for pretty printing
Rich is a terminal formatting library.
It bundles many tools for prettifying terminal output and can be installed with pip install rich
.
Rich’s print()
function is useful for debugging objects.
It neatly indents large, nested data structures, and adds syntax highlighting:

Cool beans.
Using from rich import print
replaces the builtin print()
function.
This is normally safe since the Rich version is designed as a drop-in replacement, but it does mean everything passed to print()
is formatted.
To preserve our application’s non-debug output exactly, we can use an import alias like from rich import print as rprint
, keeping rprint()
for debugging.
For more info see the Rich quick start guide.
If you’re not at liberty to install Rich, you can use Python’s pprint()
function instead.
The extra “p” stands for “pretty”.
pprint()
also indents data structures, albeit without colour or style:
>>> from pprint import pprint >>> pprint(luke) {'birth_year': '19BBY', 'films': [3, 4, 5, 6, 7, 8], 'id': 1, 'name': 'Luke Skywalker'}
Handy.
4. Use locals()
to debug all local variables
Local variables are all the variables defined within the current function.
We can grab all the local variables with the locals()
builtin, which returns them in a dictionary.
This is convenient for debugging several variables at once, even more so when combined with Rich or pprint.
We can use locals()
like so:
from rich import print as rprint def broken(): numerator = 1 denominator = 0 rprint("👉", locals()) return numerator / denominator
When we run this code, we see the dictionary of values before the exception:
>>> broken() 👉 {'numerator': 1, 'denominator': 0} Traceback (most recent call last): ... ZeroDivisionError: division by zero
There’s also the globals()
builtin which returns all the global variables, that is, those defined at the module scope, such as imported variables and classes.
globals()
is less useful for debugging since global variables don’t usually change, but it is good to know about.
5. Use vars()
to debug all of an object’s attributes
The vars()
builtin returns a dictionary of an object's attributes.
This is useful when we want to debug many attributes at once:
>>> rprint(vars(widget)) {'id': 1, 'name': 'Batara-Widget'}
Brillo.
(vars()
without an argument is also equivalent to locals()
, in about half the typing.)
vars()
works by accessing the __dict__
attribute of the object.
Most Python objects have this as the dictionary of their (writeable) attributes.
We can also use __dict__
directly, although it’s a little more typing:
>>> rprint(widget.__dict__) {'id': 1, 'name': 'Batara-Widget'}
vars()
and __dict__
don’t work for every object.
If an object’s class uses __slots__
, or it’s built in C, then it won’t have a __dict__
attribute.
6. Debug your filename and line number to make returning there easy
Update (2021-10-09): Thanks to Lim H for the tip.
Many terminals allow us to open on filenames from output.
And text editors support opening files at a given line when a colon and the line number follows the filename.
For example, this output would allow us to open example.py
, line 12:
$ echo "./example.py:12" ./example.py:12
In iTerm on macOS, we can command-click the output to open the file. Check your terminal’s menus and documentation.
We can use this capability in our print()
calls to ease reopening the code we’re trying to debug:
print(f"{__file__}:12 {widget_count=}")
This uses the Python magic variable __file__
to get the name of the current file.
We have to provide the line number ourselves.
Running this we see:
$ python example.py /Users/me/project/example.py:12 widget_count=9001
And in iTerm we can command-click the start of the line to jump right back to the broken code.
I’m told that to open in VSCode specifically, you can output a link like vscode://file/<filename>
.
✨Bonus✨ 7. Try icecream
Update (2021-10-09): Thanks to Malcolme Greene for reminding me of this package.
The icecream package provides a handy debugging shortcut function, ic()
.
This function combines some of the tools we’ve been looking at.
Called without arguments, ic()
debugs details about where and when it was called:
from icecream import ic def main(): print("Starting main") ic() print("Finishing") if __name__ == "__main__": main()
$ python example.py Starting main ic| example.py:6 in main() at 11:28:27.609 Finishing
Called with arguments, ic()
inspects and prints each expression (through source inspection) and its result:
from icecream import ic def main(): a = 1 b = 2 ic(a, b, a / b) if __name__ == "__main__": main()
$ python example.py ic| a: 1, b: 2, a / b: 0.5
This includes some syntax highlighting like Rich.
icecream also has some other handy abilities like installing as a builtin so you don’t need to import it. Check out its documentation for more info.
📙👉Speed Up Your Django Tests👈📙
One summary email a week, no spam, I pinky promise.
Related posts:
Tags: python
© 2021 All rights reserved.
::...免责声明:
当前网页内容, 由 大妈 ZoomQuiet 使用工具: ScrapBook :: Firefox Extension 人工从互联网中收集并分享;
内容版权归原作者所有;
本人对内容的有效性/合法性不承担任何强制性责任.
若有不妥, 欢迎评注提醒:
或是邮件反馈可也:
askdama[AT]googlegroups.com
订阅 substack 体验古早写作:
点击注册~> 获得 100$ 体验券:
关注公众号, 持续获得相关各种嗯哼:
自怼圈/年度番新

关于 ~ DebugUself with DAMA ;-)
粤ICP备18025058号-1
公安备案号: 44049002000656 ...::