Modern terminals are capable of far more than plain monochrome text: true
color, emphasis, layout, and live animation. We spend our whole day staring at
console output, so why should it look like it's still 1985?
Color alone makes a surprising difference. Compare IPython with and without
syntax highlighting:
Layout matters just as much. Ideally, a single trivial call should already tell
you where the output came from, what the variable was called, and how the
object is structured:
That is exactly what Neoprint is
built for. It is a drop-in replacement for print (almost the same API) that
emits colorful, neatly re-formatted output in any modern terminal.
Why another print library?
There are plenty of great logging and pretty-printing libraries out there, but
they usually ask you to change the way you work: switch to a logger, register
a formatter, adopt a whole framework. Neoprint takes the opposite approach.
Keep writing print(...) like you always have, and get the extras for free:
- Colors and styles — from BBCode-ish inline markup to 256-color and true-color output.
-
Variable names for free —
print(x, y, ':n')showsx = ...; y = ...without you repeating yourself. -
Readable object dumps — with
:l, nested lists, dicts, and dataclasses are laid out so you can actually read them. - Where did this print come from? — file and line are shown automatically.
- Progress bars and spinners — built in, no extra dependency.
- Pretty tracebacks — bring the Rich traceback panel to any exception with a single flag.
All of this with exactly one dependency: rich.
Installation
pip install neoprint
Neoprint supports Python 3.9 and above, and pulls in nothing but rich.
Quick start
Use it in a single script
from neoprint import print
print('hello world')
That's it — same name, same call signature. You can drop it in front of any
existing script and immediately get colored, annotated output.
Make it global across a package
If you want every module under a package to use the enhanced print without
editing each import, call setup() once in the package's __init__.py:
# in your some_package/__init__.py, write this:
import neoprint
neoprint.setup()
# then the regular exports
from .foo import bar
...
From that point on, every print statement inside some_package/** is enhanced
automatically. No monkey-patching in user code, no per-file boilerplate.
A taste of what you get
Because it is just print, you can adopt Neoprint incrementally. Extra
features are unlocked by a tiny "mark" string you append to the call (or pass
as the first argument) — nothing else changes:
from neoprint import print
name = 'Alice'
age = 30
print(name, age, ':n') # shows: name = "Alice"; age = 30
Nested structures can be re-flowed into a readable tree instead of one endless
line, just by adding :l:
data = {'users': [{'name': 'Alice', 'tags': ['dev', 'ops']},
{'name': 'Bob', 'tags': ['design']}]}
print(data, ':l')
And when something goes wrong, you get a proper traceback panel rather than a
bare Traceback (most recent call last): dump:
try:
1 / 0
except ZeroDivisionError as e:
print(e, ':e')
If you want more, there are opt-in marks for indexing (:i), timing (:t),
verbosity levels (:v), inline markup, progress bars, and more — but you never
have to learn them to get started. Plain print already works better.
Learn more
Neoprint is open source. You can find it on GitHub:
https://github.com/likianta/neoprint.
The full documentation lives here: https://likianta.github.io/neoprint/
If it saves you a little time in your terminal, a star on GitHub goes a long
way — and issues or PRs are always welcome.



Top comments (1)
Hey Likianta! 👋
Really interesting project! I spend a lot of time in terminals, so I can definitely appreciate the idea of making everyday debugging output more informative without introducing an entirely new workflow. 😊
I downloaded the repository and spent some time exploring the implementation. I particularly liked the AST-based variable name extraction and the way you’ve separated formatting, rendering, and caller information.
I also ran into a couple of things worth mentioning, if you don’t mind.
First, the print() compatibility: calls using standard arguments such as sep, end, file, or flush currently raise TypeError. I understand that Neoprint isn’t intended to be a perfect replacement, but supporting these would make incremental adoption much smoother.
Second, I noticed that setup() replaces builtins.print globally and filters calls based on their originating package. That’s a clever approach, although unload() might be worth revisiting to ensure it doesn’t accidentally overwrite another library’s modifications to builtins.print.
I also tried running some of the tests and encountered collection errors involving MarkupParser and the read-only debugger.enabled property. They may simply be tests that haven’t caught up with the current API.
These are just observations from exploring the code, not criticisms of the overall idea. There’s clearly a lot of thought behind this project, and I appreciate the effort you’ve put into making terminal debugging more expressive.
Thanks for sharing your work, and keep building!