Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Neoprint

Neoprint is a drop-in replacement for Python’s built-in print, made for modern consoles. It keeps the API you already know, and adds colors, source locations, variable names, readable object layouts, progress bars, and pretty tracebacks on top of it.

import neoprint as np
name = 'Alice'
age = 30
np.show(name, age, ':n')
main.py:4 | name = "Alice"; age = 30

Neoprint output

The same line with built-in print would be a plain Alice 30, invented by you and understood by no one six months later.

What you get

  • Drop-in API. neoprint.print is neoprint.show; import it as print and existing calls keep working.
  • Source locations. Every message is prefixed with filename:lineno, so you always know which line spoke.
  • Color everywhere. True-color text, BBCode-like inline markup, bright and dim variants, and proper red/green highlighting for booleans.
  • Variable names and expressions. np.show(len(names), ':n') prints len(names) = 2.
  • Readable containers. Nested lists and dicts can be expanded into a tree, a key-value table, or a grid.
  • Progress bars. Both with np.Progress(...) and for x in np.progress(...) are built in.
  • Pretty exceptions. One mark turns an exception into a Rich traceback panel, optionally with local variables.
  • One dependency. Only rich.

Installation

pip install neoprint

Python 3.9 or newer is required.

Hello world

from neoprint import print
print('hello world')
main.py:2 | hello world

That is the whole story for a single script. Continue with Installation and Hello World to learn the package-wide setup() mode, or jump to Printing Basics for the anatomy of a message.

How this manual is organized

  • Getting Started covers installation and the anatomy of a message.
  • Markup explains the :xN marks, which are the main extension point of the library, and lists every mark in one table.
  • Guides walks through each feature with runnable examples: verbosity, variable names, expanded formatting, rich text, index counters, timing, dividers, exceptions, source layers, and progress bars.
  • Reference documents the configuration switches and the public API.

If you only read two pages, read Printing Basics and Markup Reference.

Installation and Hello World

Requirements

  • Python 3.9 or newer
  • A modern terminal. Windows Terminal, iTerm2, GNOME Terminal, and the integrated terminals of VS Code and PyCharm all work. If colors look wrong on an old Windows console, see Configuration.

Install

pip install neoprint

The only runtime dependency is rich.

Hello world

import neoprint as np

np.show('hello world')

Hello world output

The message is made of two parts:

  • a head — main.py:3 — showing where the call happened,
  • a body — hello world — the text you passed.

A thin | separator sits between them. Both parts are colored: the file name is blue, the line number green, the separator dim.

Use it as print

neoprint.print is a plain alias of neoprint.show, so you can keep the name you are used to:

from neoprint import print

print('hello world')

If you want to swap it in for an entire script without touching every call, simply shadow the built-in:

from neoprint import print

print('this is neoprint now', 1, 2, 3)

Note that the built-in print accepts keyword arguments such as file= and end=. Neoprint’s print does not; for those cases use the mark :s2 (see Printing Basics) or the real builtins.print.

Enable it for a whole package

Editing imports in every module is noisy. Call setup() once in your package’s __init__.py:

# some_package/__init__.py
import neoprint
neoprint.setup()

# regular exports
from .foo import bar

From then on, every built-in print(...) call inside some_package is routed through neoprint automatically. Calls in other packages, and in the standard library, keep using the built-in print — setup() patches the built-in only for the package that called it.

To undo the patch:

import neoprint
neoprint.unload()

Try the examples

The repository ships runnable examples:

python examples/hello_world.py
python examples/all_markup_usage.py
python examples/progress.py

Next: Printing Basics.

Printing Basics

Arguments are joined with semicolons

Multiple positional arguments are printed one after another, separated by a dim ; and a space:

import neoprint as np

np.show('Multiple', 'arguments', 'are', 'semicolon-separated')
np.show(123, 456, 'mixed types', True, False, None)

Semicolon-joined arguments

Booleans are highlighted: True is green, False is red, None is magenta.

If you want a different join style, pass :l (one value per line, see Expanded Formatting) or format the string yourself and pass a single argument.

Where to put a markup

A markup is a short string such as ':n', ':v8' or ':d2v7t' that changes how the message is rendered. Each character in it enables one feature; an optional digit selects a variant.

A markup is recognized in exactly two positions:

  1. as the first positional argument,
  2. as the last positional argument.
np.show(':d', 'Section title')          # first
np.show('Section title', ':d')          # last
np.show('Section title', markup=':d')   # keyword

Anything else is treated as ordinary text:

np.show('a', ':n', 'b')   # ':n' is printed literally
main.py:1   | a; :n; b

The string must look like a markup to be treated as one: it has to start with : and contain only the known mark letters. Something like ':)' or ': not a markup' is plain text.

Combining marks

Marks can be stacked in a single string. Each mark letter may carry at most one digit:

np.show(':d2v7t', 'End')
main.py:1   | [16:57:41 -> 16:57:41 (4ms)] ██████████ End ██████████

Read it as :d2 + :v7 + :t. Order does not matter.

Bare marks and their defaults

Writing a mark without a digit selects its default variant. For example :i is :i1 and :e is :e2:

MarkBare form means
:d:d1 thin divider
:e:e2 traceback panel
:i:i1 line-level counter
:l:l1 expanded format
:n:n1 show variable names
:p:p1 parent frame
:r:r1 inline markup enabled
:s:s1 short form
:t:t2 time since last mark
:v:v0 debug level

The full list is in Markup Reference.

Quiet and plain modes

Two marks reduce or remove neoprint’s decoration:

  • :s1 keeps colors but drops the source head and variable names.
  • :s2 hands the arguments to the built-in print untouched.
np.show(':s1', 'no source, no varnames')
np.show(':s2', 'built-in print behaviour')
no source, no varnames
built-in print behaviour

:s2 is handy when a call must accept the same keyword arguments as the built-in print (for example file=sys.stderr).

A note on string formatting

Neoprint follows the convention of this project: prefer str.format over f-strings, because f-strings in log calls are evaluated even when the output is filtered by verbosity.

np.show(':v1', 'loaded {} items'.format(count))

Next: How Markup Works.

How Markup Works

Neoprint keeps the print signature. Instead of adding dozens of keyword arguments, it adds one optional string — the markup — that switches features on.

Grammar

A markup starts with : and is a concatenation of mark letters, each optionally followed by a single digit:

:n          one mark, default variant
:v8          one mark, variant 8
:i0         reset the index counter
:d2v7t      three marks combined

The valid letters are d, e, f, i, l, n, p, r, s, t, v. Digits are 0-9.

A string is only treated as a markup when it matches this grammar exactly and is in the first or last positional slot (or passed via markup=). Otherwise it is printed literally, so ordinary text like ':' or ':)' never gets misinterpreted.

What a mark can change

Every mark affects one of three layers of the output:

LayerMarksExamples
The head (left of the separator)pshow the caller of the caller
The prefix inside the bodyi, t[3] index, [12:00 -> 12:01 (1m)] timing
The values and layoutd, e, l, n, r, s, vcolors, tables, dividers, tracebacks

Order of marks does not matter

:tiv2 and :v2it are the same markup. What matters is the position inside the call, and the digits.

Some pairs conflict

  • :d (divider style) and :l (expanded format) cannot be used together. A divider wraps the body in a single line, an expanded format spreads it over many; neoprint rejects that combination with an AssertionError.
  • :r2 and :l2 mean the same thing. When both are given, the :l one is kept.
  • :d overrides :r2 by turning it off.

When in doubt, enable one layout mark at a time.

Marks are per call

Marks do not stick. np.show('x', ':v8') prints one red line; the next call starts from the defaults again. The only stateful features are the index counters (:i) and timers (:t), and they have explicit reset marks (:i0, :t0).

Where to go next

  • Markup Reference — every mark, every variant.
  • Individual guides under Guides explain each feature in depth.

Markup Reference

One table, every mark. The Default column marks the variant used when the letter is written without a digit (* = the bare form selects this variant).

Divider d

DefaultMarkDescription
:d0no divider
*:d1thin divider line
:d2thick divider line

See Dividers.

Exception e

DefaultMarkDescription
:e0no special handling; print as ordinary value
:e1print the exception message in red
*:e2Rich traceback panel
:e3Rich traceback panel with locals (show_locals)
:e4reserved, not implemented

See Exception Printing.

Flush f

DefaultMarkDescription
*:f1reserved

The f mark is parsed for forward compatibility but currently has no effect on the output. Do not rely on it.

Index i

DefaultMarkDescription
:i0reset counters
*:i1line-level counter (one counter per source line)
:i2scoped counter (per function, or per np.scope)
:i3global counter

See Index Counters.

Layout l

DefaultMarkDescription
:l0inline representation
*:l1expand containers over multiple lines
:l2tailored layout (tables, key-value grids, a -> b transforms)

See Expanded Formatting.

Varnames n

DefaultMarkDescription
:n0hide variable names
*:n1show variable names and expressions

See Variable Names.

Parent layer p

DefaultMarkDescription
:p0head points at the current frame
*:p1head points at the caller
:p2caller’s caller
:p3+one frame further up per number

See Source Layers.

Rich text r

DefaultMarkDescription
:r0inline markup disabled
*:r1inline markup enabled ([red]...[/])
:r2same as :l2

See Rich Text.

Short s

DefaultMarkDescription
:s0full output
*:s1short form: no source head, no variable names
:s2delegate to the built-in print

See Printing Basics.

Timing t

DefaultMarkDescription
:t0reset the timer
:t1absolute timestamp
*:t2time since the previous :t2/:t3 call
:t3time since the scope timer was reset

See Timing.

Verbosity v

DefaultMarkDescriptionColor
*:v0debuggray
:v1negative infomagenta
:v2positive infoblue
:v3weak successdim green
:v4successgreen
:v5weak warningdim yellow
:v6warningyellow
:v7weak errordim red
:v8errorred
:v9criticalreserved, not implemented

See Verbosity Levels.

Verbosity Levels

The v mark colors a message according to its severity. The scale runs from v0 (debug) to v8 (error); odd numbers are dim variants of the even one above them.

import neoprint as np

np.show(':v0', 'v0 debug')
np.show(':v1', 'v1 negative info')
np.show(':v2', 'v2 positive info')
np.show(':v3', 'v3 weak success')
np.show(':v4', 'v4 success')
np.show(':v5', 'v5 weak warning')
np.show(':v6', 'v6 warning')
np.show(':v7', 'v7 weak error')
np.show(':v8', 'v8 error')
LevelMeaningColor
v0debuggray
v1negative or serious infomagenta
v2positive or neutral infoblue
v3weak successdim green
v4successgreen
v5weak warningdim yellow
v6warningyellow
v7weak error / failuredim red
v8error / failurered

Verbosity levels

The color covers the whole body of the message, not the source head.

Shorthand functions

For the levels you use most, import the ready-made helpers:

from neoprint import debug, info, success, warning, error

debug('user has come here')
info('welcome to the city')
success('file created')
warning('target file exists, will overwrite it')
error('failed to create the file')

Each helper is just a call with the matching mark already applied, so marks can still be appended:

error('failed: {}'.format(err), ':d')   # red message wrapped by dividers

The helpers are functools.partial objects, not separate implementations, so they accept the same arguments as np.show.

Choosing a level

A useful rule of thumb:

  • v0/v1 for traces you read while developing;
  • v2/v4 for confirmations the user of your program cares about;
  • v6 for recoverable surprises;
  • v8 for failures that stop the current task.

If you only need colored text and not a semantic level, prefer inline markup in :r1 mode — see Rich Text.

Variable Names

The n mark prints each argument next to its source expression. Neoprint reads the source line you called it from, so you never have to type 'name =' yourself.

import neoprint as np

name = 'Alice'
age = 30
city = 'New York'
np.show(name, age, city, ':n')
main.py:6   | name = "Alice"; age = 30; city = "New York"

Expressions work too

Plain names are only the beginning. Function calls and attribute accesses are also reconstructed:

names = ['Alice', 'Bob']
np.show(len(names), ':n')
class Box:
    def __init__(self):
        self.value = 42

box = Box()
np.show(box.value, ':n')

Variable names and expressions

Enable it globally

If you want variable names on every call without repeating the mark, turn on the configuration switch:

import neoprint as np

np.config(show_varnames=True)

name = 'Alice'
np.show(name)       # name = "Alice"

Restore the default with np.config(show_varnames=False) or np.config.reset().

Limitations

  • The function has to keep a recognizable name. Neoprint only inspects calls to print, show, debug, info, success, warning, error, format, and format_list. If you alias the import, for example from neoprint import show as s, the call no longer looks like one of those names and the mark cannot resolve the arguments. Use from neoprint import print (the name is print) instead of renaming.
  • Literals have no name. np.show(42, ':n') prints 42, because there is nothing to reflect.
  • Truthiness of the mark. :n0 hides names, :n1 (bare :n) shows them.

How it works, briefly

The source file of the calling frame is parsed with ast and cached per modification time. The analyzer finds the Call node on the reported line and reads the source segment of each argument. That is why the names are exactly what you wrote — including spacing inside longer expressions — and why the feature costs one file parse per source change, not per call.

See also Source Layers for how the head of a message is resolved.

Expanded Formatting

By default neoprint prints values the way repr would — on one line. The l mark trades width for readability.

:l0 — inline (default)

import neoprint as np

data = {'users': [{'name': 'Alice', 'tags': ['dev', 'ops']},
                  {'name': 'Bob', 'tags': ['design']}]}
np.show(data)
main.py:5   | {'users': [{'name': 'Alice', 'tags': ['dev', 'ops']}, {'name': 'Bob', 'tags': ['design']}]}

Readable for a glance, painful for anything deeper.

:l1 — expand over multiple lines

:l1 (or the bare :l) walks the container recursively and indents each level by two spaces:

np.show(data, ':l')

Expanded tree

Strings are quoted, trailing commas are added, and the indentation is controlled by np.config(multiline_indent=N) (default 2).

:l2 — tailored layouts

Certain shapes get a purpose-built layout instead of the generic tree. If an object does not match any of them, :l2 falls back to :l1 behaviour.

Sequence of rows → grid

A list or tuple whose first row is a header of strings becomes a table:

np.show(':l2', [('id', 'name'), ('1', 'Alice'), ('2', 'Bob')])
main.py:1   |
  ╭────┬───────╮
  │ id │ name  │
  ├────┼───────┤
  │ 1  │ Alice │
  │ 2  │ Bob   │
  ╰────┴───────╯

dict[str, str] → key-value table

np.show(':l2', {'name': 'neoprint', 'version': '1.0.0'})
main.py:1   |
  ╭─────────┬──────────╮
  │ KEY     │ VALUE    │
  ├─────────┼──────────┤
  │ name    │ neoprint │
  │ version │ 1.0.0    │
  ╰─────────┴──────────╯

a -> b strings → colored transform

A string containing -> is rendered as a red-to-green transition, with an optional label before a colon:

np.show(':l2', 'v0.1.0 -> v0.2.0')
np.show(':l2', 'status: draft -> published')
main.py:1   | v0.1.0 -> v0.2.0
main.py:2   | status: draft -> published

(The left side is red, the right side green, the label stays default-colored.)

Tailored layouts

Shorthand functions

np.expand(x) is np.show(x, ':l'), and np.expand2(x) is np.show(x, ':l2'):

from neoprint import expand, expand2

expand([1, 2, 3])
expand2([('k', 'v'), ('a', 'b')])

Note that :l cannot be combined with :d on the same call; see How Markup Works.

Rich Text

The r mark controls inline styling inside the strings you print.

:r1 — inline markup

With :r1 (or bare :r), text enclosed in BBCode-like tags is colored and styled:

import neoprint as np

np.show(':r1', '[red]red[/] [green bold]green bold[/] [dim]dim[/]')
main.py:3   | red green bold dim

The console output is colored; the plain rendering above shows the text only.

Tags

A tag is [ + space-separated parts + ]. Parts are either a color name or a style name:

  • colors: black, red, green, yellow, blue, magenta, cyan, white, default, and the bright_* variants of the first eight;
  • styles: bold, dim, italic, underline, plus the short forms b, i, u.

Close a tag with [/]. Tags nest, and unclosed attributes inherit from the enclosing tag:

np.show(':r1', '[red bold]both[/] [red]red only[/]')
np.show(':r1', '[red bold]outer [green]green keeps bold[/][/]')

To print a literal [, escape it: \[.

Rich text styles

:r2 — tailored layout

:r2 is an alias of :l2: it applies the special layouts described in Expanded Formatting (grids, key-value tables, a -> b strings). Use it when you think of the flag as “render this nicely” rather than “expand this structure”; the result is the same.

Markdown

For long-form text, neoprint offers a dedicated helper:

import neoprint as np

np.markdown(
    '''
    # Title

    - item 1
    - item 2
    '''
)

The string is rendered by Rich’s Markdown renderer, with headings, lists, and inline code styled. An optional markup can be given as the first argument to add a source head or other prefixes:

np.markdown(':d', '# Chapter 1')

Pre-colored segments

For programmatic use — building a message from pieces with their own colors — neoprint exposes a small text object:

import neoprint as np

np.show(np.Text('plain text object', 'cyan', 'bold'))

Text(value, color, style) accepts the same color and style names as the inline markup. It is exported at the top level as np.Text.

Index Counters

The i mark attaches a running counter to the message, so sequences are easy to follow without writing print(i, ...).

import neoprint as np

np.show(':i2', 'first')
np.show(':i2', 'second')
np.show(':i2', 'third')

Index counters

Every counter is rendered as [n] before the body, in a color derived from its frame, so values stand out from the text.

The four counters

MarkScopeCounter increments
:i1one per source lineevery time that line runs
:i2one per scopeevery call in the same function, class body, module, or np.scope() block
:i3globalevery call, anywhere
:i0—resets all counters

Bare :i is :i1.

Line-level (:i1)

Best for loops and repeated single lines:

for _ in range(3):
    np.show(':i1', 'line')
main.py:2   | [1] line
main.py:2   | [2] line
main.py:2   | [3] line

Two different lines each start at [1], because they own separate counters.

Scoped (:i2)

Best when several lines form one logical stream. The scope is the enclosing function (or class body, or module):

def stage():
    np.show(':i2', 'step one')
    np.show(':i2', 'step two')

stage()
stage()
main.py:3   | [1] step one
main.py:4   | [2] step two
main.py:3   | [3] step one
main.py:4   | [4] step two

Global (:i3)

A single counter shared by the whole program:

np.show(':i3', 'G1')
np.show(':i3', 'G2')
main.py:1   | [1] G1
main.py:2   | [2] G2

Resetting

np.show(':i0')      # reset every counter
np.show(':i', 'back to [1]')

Custom scopes

np.scope() opens a named region. Inside it, :i2 counters are keyed by the scope name instead of the function, which lets unrelated blocks share — or deliberately not share — a counter:

with np.scope(name='download'):
    np.show(':i2', 'file A')
    np.show(':i2', 'file B')

with np.scope():                 # anonymous scope
    np.show(':i2', 'anonymous 1')

Custom scopes

Scopes also give :t3 its own timeline; see Timing.

Colors

Counter colors are generated per source frame from a bright hex palette and rendered with terminal true color. You do not choose them; they only serve to tell frames apart. :i3 is always red.

Shorthand

np.index(x) is np.show(x, ':i2').

Timing

The t mark appends a timestamp to the message. It is useful for tracing how long stages take without exporting metrics.

from time import sleep
import neoprint as np

np.show(':t', 'starting')
sleep(0.2)
np.show(':t', 'loaded config')
sleep(0.5)
np.show(':t', 'connected')

Timing output

The prefix shows the previous timestamp, the current one, and the delta. Formatting scales automatically: milliseconds for short gaps, seconds, minutes, hours for long ones.

The four variants

MarkMeans
:t1absolute timestamp only, no delta
:t2time since the previous :t2/:t3 call (bare :t)
:t3time since the scope timer started, or since :t0
:t0reset the timer

:t2 and :t3 share one “instant” pointer, so alternating them measures each step against the previous call. :t0 moves that pointer to now and restarts the current scope’s timer.

Scopes

Inside np.scope(), :t3 measures from the moment the scope was entered:

with np.scope(name='job'):
    np.show(':t3', 'start')
    sleep(1)
    np.show(':t3', 'one second into the job')

Outside a scope, :t3 measures from the most recent :t0.

Practical pattern

:t is most useful as a drop-in replacement for ad-hoc time.perf_counter() logging:

np.show(':t', 'begin download')
data = download()
np.show(':t', 'downloaded {} bytes'.format(len(data)))
np.show(':t', 'done')

Dividers

The d mark frames a message with a horizontal rule. It is a lightweight way to separate sections in a long run of output.

import neoprint as np

np.show(':d', 'Section title')
np.show('some line')
np.show('another line')
main.py:3   | ──────────────────── Section title ─────────────────────
main.py:4   | some line
main.py:5   | another line

The rule spans the console width; the text you passed sits in the middle.

Variants

MarkLook
:d1thin line (bare :d)
:d2thick block line
np.show(':d1', 'thin')
np.show(':d2', 'thick')

Divider variants

Interaction with other marks

  • :d cannot be combined with :l — a divider needs a single-line body. Combining them raises an AssertionError; see How Markup Works.
  • :d turns :r2 off, because :r2 produces multi-line layouts too.
  • Any other mark, such as :v8 or :i, still applies: np.show(':d2v7t', 'End') yields a thick divider with a timestamp, rendered in the v7 (dim red) style.

Shorthand

from neoprint import divider

divider('Section title')

Exception Printing

The e mark decides what happens when an exception object is among the arguments.

:e0 — plain

No special handling; the exception is printed like any other value.

import neoprint as np

try:
    1 / 0
except ZeroDivisionError as e:
    np.show(e, ':e0')
main.py:6   | division by zero

:e1 — red message

The message is printed in red, without a traceback:

np.show(e, ':e1')

Use it when you only want to signal a failure, not explain it.

:e2 — traceback panel (default)

This is what bare :e does. The exception is rendered through Rich’s traceback renderer:

np.show(e, ':e2')

The panel includes the source snippet of every frame, with the failing line marked.

Traceback panel

:e3 — traceback with locals

Same as :e2, plus the local variables of each frame:

np.show(e, ':e3')

Useful when the bug depends on values you cannot guess from the code. Locals whose names start with dunder or sunder (__x__, _x_) are hidden.

Printing the current exception

Inside an except block you can omit the argument entirely; neoprint reads the current exception from sys.exc_info():

try:
    load_config()
except Exception:
    np.show(':e')       # same as np.show(':e2')

Combining with other marks

e combines freely with v, d, and i:

np.show(e, ':v8')       # one red line, no traceback

Shorthand

from neoprint import exception

try:
    ...
except Exception as e:
    exception(e)        # same as np.show(e, ':e2')

Global excepthook

When config.rich_traceback is on (the default), uncaught exceptions are printed by Rich’s excepthook instead of Python’s bare traceback, without any call to np.show. Set show_traceback_locals to include locals there too:

import neoprint as np

np.config(show_traceback_locals=True)

See Configuration.

Source Layers

Every message starts with a head like main.py:10. By default that head points at the line that called np.show. The p mark moves the head up the call stack, so a shared logging helper can report the place that used it.

The stack

p0 is the frame of the np.show call itself, p1 is its caller, p2 its caller’s caller, and so on. Bare :p is :p1.

import neoprint as np            # ln1

def aaa():                       # ln3
    def bbb():                   # ln4
        def ccc():               # ln5
            np.show('DDD', ':p0')    # ln7
            np.show('CCC', ':p1')    # ln8
            np.show('BBB', ':p2')    # ln9
            np.show('AAA', ':p3')    # ln10
        ccc()                    # ln12
    bbb()                        # ln13
aaa()                            # ln14

Source layers

p0 reports the line that printed; p1 reports the line that called that function; and so on. Going further than the stack depth raises an error, so the practical range is p0 to p3.

Why it matters

Consider a library method that is called from many places. With :p, each message points at the caller, not at the library:

import neoprint as np

class Library:
    def borrow_book(self, book_name):
        np.show('borrowed "{}"'.format(book_name), ':p')
        return Book(book_name)

lib = Library()

# in aaa.py
lib.borrow_book('Stray Birds')

# in bbb.py
lib.borrow_book('How the Steel Was Tempered')
aaa.py:8    | borrowed "Stray Birds"
bbb.py:10   | borrowed "How the Steel Was Tempered"

That is how you trace which script triggered a shared code path.

Relationship with varnames

:p only moves the head (the source prefix). Variable names from :n are always read from the line where np.show is written, regardless of :p. In a shared helper, the values passed to the helper still come from the helper’s own line, while the head shows the outside caller.

Note that p2 and above rely on the interpreter’s stack being intact. Inside heavy wrappers or asynchronous code, prefer :p1.

Progress Bars

Neoprint ships two forms of progress display: an object you drive manually (np.Progress) and a generator you wrap around an iterable (np.progress).

np.Progress

from time import sleep
import neoprint as np

names = ['Alice', 'Bob', 'Carol']

with np.Progress(total=len(names)) as prog:
    for name in names:
        prog.update(name)
        sleep(0.05)

Progress bar

The bar fills with ─, the counter shows index/total, and update(label) prints a label on the right, truncated to the remaining width. The head (main.py:6) comes from the with line.

total is required

update() decides the bar type on its first call. If total is still None at that point, neoprint raises NotImplementedError — the spinner-style indeterminate bar is not implemented yet. Set total either in the constructor or as a property before the first update():

with np.Progress() as prog:
    prog.total = len(items)
    for item in items:
        prog.update()

Setting total after the first update() raises ValueError.

Indicators

indicator_style selects the read-out:

StyleExample
'counter' (default)[ 42/100]
'digital'( 42%)
'decimal'( 42.00%)
'none'bar only

color accepts 'red' (default), 'green', 'yellow', or 'blue'.

with np.Progress(total=100, indicator_style='decimal', color='green') as p:
    ...

Progress indicators

np.progress

np.progress(iterable) wraps an iterable in a Progress block and yields its items. For sized containers the total is inferred; for generators pass it explicitly:

for name in np.progress(names):
    sleep(0.05)

def gen():
    for name in names:
        yield name

for name in np.progress(gen(), total=len(names)):
    sleep(0.05)

Plain items only update the counter. To show a label, wrap the value in np.ProgressItem:

for name in np.progress([np.ProgressItem(n) for n in names]):
    sleep(0.05)

ProgressItem(data, label='') yields data from the loop while displaying label (defaults to str(data)).

Notes

  • The bar width adapts to the console width, measured when the Progress object is created.
  • close() (called by the with block) resets the object, so a Progress instance can be reused.
  • In debug mode (np.config(debug_print=True)), the bar prints one line per update instead of overwriting; useful when output is captured to a file.
  • np.Spinner, np.spinner, and Progress without total are placeholders; do not rely on them yet.

A combined example

import neoprint as np
from time import sleep

items = ['a', 'b', 'c']
np.show(':d', 'downloading')

with np.Progress(total=len(items), color='green') as p:
    for item in items:
        p.update(item)
        sleep(0.05)

np.show(':d2v4t', 'done')

Configuration

Global switches live in a single object, np.config. Call it with keyword arguments to change settings, and np.config.reset() to restore defaults.

import neoprint as np

np.config(show_varnames=True)
...
np.config.reset()

Unknown names raise AssertionError, which catches typos early.

Options

OptionDefaultEffect
show_sourceTrueshow the filename:lineno head
show_varnamesFalsealways show variable names, as if :n were passed
disable_varnamesFalsenever show variable names, and skip source parsing
console_widthterminal widthwidth used for dividers, progress bars, wrapping
multiline_indent2spaces per nesting level in expanded output
legacy_windowsFalsereduce color effects on legacy consoles
rich_tracebackTrueinstall Rich’s excepthook for uncaught exceptions
show_traceback_localsFalseinclude locals in uncaught-exception panels
sourcemap_alignment'left'pad line numbers so heads align
debug_printFalseverbose internal logging of neoprint itself
debug_outputFalsecollect printed output for tests

A few options are accepted but have no visible effect yet: show_funcname, show_verbosity_tag, path_style, clear_unfinished_stream, subthreaded. They are reserved for upcoming releases.

Environment variables

Some switches can be set before the interpreter imports neoprint:

VariableEquivalent
NEOPRINT_DEBUG=1debug_print=True
NEOPRINT_DEBUG_PRINT=1debug_print=True
NEOPRINT_DISABLE_VARNAMES=1disable_varnames=True
NEOPRINT_LEGACY_WINDOWS=1legacy_windows=True

This is useful for CI, where you may want to disable variable-name lookups without touching the code. Note that console_width has no environment variable; set it from code with np.config(console_width=100).

A typical setup

import neoprint as np

# a wide, stable layout in CI, verbose helpers locally
np.config(
    console_width=120,
    multiline_indent=2,
    show_varnames=True,
)

Debugging neoprint itself

debug_print=True makes neoprint narrate its own decisions (frame resolution, markup analysis, varname lookups) to stderr. It is meant for reporting bugs, not for daily use.

API Reference

Everything below is available from the package root:

import neoprint as np

Printing

NameSignatureEquivalent
np.showshow(*args, markup=None)the main entry point
np.printprint(*args, markup=None)alias of np.show
np.debugdebug(*args, markup=None)show(..., ':v1')
np.infoinfo(*args, markup=None)show(..., ':v2')
np.successsuccess(*args, markup=None)show(..., ':v4')
np.warningwarning(*args, markup=None)show(..., ':v6')
np.errorerror(*args, markup=None)show(..., ':v8')
np.dividerdivider(*args, markup=None)show(..., ':d1')
np.exceptionexception(*args, markup=None)show(..., ':e2')
np.expandexpand(*args, markup=None)show(..., ':l1')
np.expand2expand2(*args, markup=None)show(..., ':l2')
np.indexindex(*args, markup=None)show(..., ':i2')

All printing functions return None. A markup passed as the first or last positional argument is merged with the shorthand’s own markup, so np.warning(x, ':n') is the same as np.show(x, ':v6n').

np.vshow exists as the :n1 shorthand, but variable-name resolution keys on the called function name, and vshow is not recognizable to the analyzer. Use np.show(x, ':n1') instead.

markdown

markdown(*args: str) -> None

Accept one argument (the Markdown text) or two arguments (the text and a markup, in either order):

np.markdown('# Title\n\nSome **bold** text.')
np.markdown(':n', '**bold**')

Formatting

format(*args, markup=None) -> str

Returns the same layout that np.show would print, but with color codes stripped, so it can be logged to a file or embedded in another message.

format_list(*args, markup=None) -> list[TextObject]

The lower-level pipeline. Each returned object has a render(color_code_scheme=...) method, where the scheme is 'plain', 'ansi', or 'bbcode'. Use it when you want to post-process pieces rather than print them directly. The remaining parameters (_frame, _elevate_parent_level, _mark_position, _raise_builtin_print) are internal.

Text objects

Text(obj, color='default', style='')

A piece of text with an explicit color and style. True, False, and None are rendered green, red, and magenta by default. Colors can be names, Rich styles, or #rrggbb values.

Markdown(text)

A Rich-rendered Markdown block. Prefer np.markdown() for printing, which also takes care of the markup and line break.

Scopes and counters

scope(name='') -> contextmanager

Opens a named scope for the with block. Inside it, :i2 (and np.index) counts per scope and :t3 reports scope-level timing. Without a name, a random short id is generated.

Progress bars

Progress(total=None, indicator_style='counter', color='red')

A manual progress bar. indicator_style is one of 'counter', 'digital', 'decimal', or 'none'; color is one of 'blue', 'green', 'yellow', 'red'.

Methods:

MethodEffect
update(item=None)advance one step; item is an optional label
close()move to a new line and reset
reset()reset the counter and total
total (property)read or set the total before the first update()

total is required for a normal bar. A Progress without a total raises NotImplementedError on the first update().

progress(obj, total=None, **kwargs) -> Iterator

A generator that wraps an iterable and advances automatically. For concrete sized containers, total is inferred from len(obj); generators need an explicit total.

ProgressItem(data, label='')

Wraps one item of a progress() iteration to attach a label. The loop receives data, and the bar shows label (or str(data) when omitted).

Spinner and spinner() are exported as reserved names and are not implemented yet.

Configuration

config(**kwargs) -> None
config.reset() -> None

Change global options, or restore defaults. See Configuration for the full option table.

Package control

setup(package_name=None) -> None
unload() -> None

np.setup(__package__) replaces the builtin print with np.print for the given package, so existing print calls gain neoprint’s formatting without being edited. np.unload() restores the builtin print.

Advanced

NamePurpose
np.consolethe underlying Rich console
np.bprintthe original builtin-style print, always unformatted
np.FrameInfoframe inspection used by source prefixes and aliases
np.elevate_parent_leveladjust which frame is reported as the caller
np.debuggerinternal debugger switch object
np.text_objectall text object classes