NumPy arrays, ready for human consumption
76
stars
110
commits
Jupyter Notebook
primary language
May 15, 2026
updated
JAXpip install lovely-numpy
or
conda install -c conda-forge lovely-numpy
How often do you find yourself debugging NumPy code? You dump your variable to the cell output, and see this:
numbers
array([[[-0.3541, -0.1975, -0.6715],
[-0.3369, -0.1975, -0.9853],
...,
[-0.4739, -0.3725, -0.689 ],
[ 2.2489, 2.4111, 2.396 ]],
[[-0.4054, -0.25 , -0.7238],
[-0.4226, -0.2325, -1.0724],
...,
[-0.8507, -0.6702, -1.0201],
[ 2.1633, 2.3585, 2.3263]],
...,
[[-0.8507, -0.3901, -1.1944],
[-0.7822, -0.2325, -1.4559],
...,
[-1.5014, -1.2304, -1.4733],
[ 2.1804, 2.4111, 2.4308]],
[[-0.8335, -0.4076, -1.2293],
[-0.8164, -0.285 , -1.5256],
...,
[-1.5528, -1.2829, -1.5256],
[ 2.1119, 2.341 , 2.3611]]], shape=(196, 196, 3), dtype=float32)
Was it really useful for you, as a human, to see all these numbers?
What is the shape? The size?
What are the statistics?
Are any of the values nan or inf?
Is it an image of a man holding a tench?
from lovely_numpy import lo
Lo and behold!lo(numbers)
array[196, 196, 3] f32 n=115248 (450 KiB) xβ[-2.118 |ββ
ββββββββ| 2.640] ΞΌ=-0.388 Ο=1.073
Better, eh?
lo(numbers[1,:6,1]) # Still shows values if there are not too many.
array[6] f32 xβ[-0.408, -0.232] ΞΌ=-0.340 Ο=0.075 [-0.250, -0.232, -0.338, -0.408, -0.408, -0.408]
spicy = numbers[0,:12,0].copy()
spicy[0] *= 10000
spicy[1] /= 10000
spicy[2] = float('inf')
spicy[3] = float('-inf')
spicy[4] = float('nan')
spicy = spicy.reshape((2,6))
lo(spicy) # Spicy stuff
array[2, 6] f32 n=12 xβ[-3.541e+03, -3.369e-05] ΞΌ=-393.776 Ο=1.113e+03 +Inf! -Inf! NaN!
lo(np.zeros((10, 10))) # A zero array - make it obvious
array[10, 10] n=100 all_zeros
lo(spicy, verbose=True)
array[2, 6] f32 n=12 xβ[-3.541e+03, -3.369e-05] ΞΌ=-393.776 Ο=1.113e+03 +Inf! -Inf! NaN!
array([[-3540.5432, -0. , ..., nan, -0.4054],
[ -0.4226, -0.4911, ..., -0.5424, -0.5082]],
shape=(2, 6), dtype=float32)
.deeperlo(numbers.transpose(2,1,0)).deeper
array[3, 196, 196] f32 n=115248 (450 KiB) xβ[-2.118 |ββ
ββββββββ| 2.640] ΞΌ=-0.388 Ο=1.073
array[196, 196] f32 n=38416 xβ[-2.118 |βββ
βββββββ| 2.249] ΞΌ=-0.324 Ο=1.036
array[196, 196] f32 n=38416 xβ[-1.966 |βββ
βββββββ| 2.429] ΞΌ=-0.274 Ο=0.973
array[196, 196] f32 n=38416 xβ[-1.804 |ββββββββββ| 2.640] ΞΌ=-0.567 Ο=1.178
# You can go deeper if you need to
lo(numbers[:3,:4]).deeper(2)
array[3, 4, 3] f32 n=36 xβ[-1.125, -0.197] ΞΌ=-0.563 Ο=0.280
array[4, 3] f32 n=12 xβ[-0.985, -0.197] ΞΌ=-0.487 Ο=0.259
array[3] f32 xβ[-0.672, -0.197] ΞΌ=-0.408 Ο=0.197 [-0.354, -0.197, -0.672]
array[3] f32 xβ[-0.985, -0.197] ΞΌ=-0.507 Ο=0.343 [-0.337, -0.197, -0.985]
array[3] f32 xβ[-0.881, -0.303] ΞΌ=-0.530 Ο=0.252 [-0.405, -0.303, -0.881]
array[3] f32 xβ[-0.776, -0.303] ΞΌ=-0.506 Ο=0.199 [-0.440, -0.303, -0.776]
array[4, 3] f32 n=12 xβ[-1.072, -0.232] ΞΌ=-0.571 Ο=0.281
array[3] f32 xβ[-0.724, -0.250] ΞΌ=-0.460 Ο=0.197 [-0.405, -0.250, -0.724]
array[3] f32 xβ[-1.072, -0.232] ΞΌ=-0.576 Ο=0.360 [-0.423, -0.232, -1.072]
array[3] f32 xβ[-0.968, -0.338] ΞΌ=-0.599 Ο=0.268 [-0.491, -0.338, -0.968]
array[3] f32 xβ[-0.968, -0.408] ΞΌ=-0.651 Ο=0.235 [-0.577, -0.408, -0.968]
array[4, 3] f32 n=12 xβ[-1.125, -0.285] ΞΌ=-0.631 Ο=0.280
array[3] f32 xβ[-0.828, -0.303] ΞΌ=-0.535 Ο=0.219 [-0.474, -0.303, -0.828]
array[3] f32 xβ[-1.125, -0.285] ΞΌ=-0.628 Ο=0.360 [-0.474, -0.285, -1.125]
array[3] f32 xβ[-1.020, -0.390] ΞΌ=-0.651 Ο=0.268 [-0.542, -0.390, -1.020]
array[3] f32 xβ[-1.003, -0.478] ΞΌ=-0.708 Ο=0.219 [-0.645, -0.478, -1.003]
.rgb colorThe important queston - is it our man?
lo(numbers).rgb

Maaaaybe? Looks like someone normalized him.
in_stats = ( (0.485, 0.456, 0.406), # mean
(0.229, 0.224, 0.225) ) # std
# numbers.rgb(in_stats, cl=True) # For channel-last input format
lo(numbers).rgb(denorm=in_stats)
# Denorm is used to convert the input to [0..1], which is then mapped to RGB.
# lo(numbers).rgb(denorm="imagenet") # same as above
# lo(numbers).rgb(denorm="symmetric") # [-1 .. 1] input
# lo(numbers).rgb(denorm="minmax") # Use the min/max elements in each channel to scale the input to [0..1]

Itβs indeed our hero, the Tenchman!
.chans# .chans will map values betwen [-1,1] to colors.
# Make our values fit into that range to avoid clipping.
mean = np.array(in_stats[0])
std = np.array(in_stats[1])
numbers_01 = (numbers*std + mean).clip(0,1)
lo(numbers_01)
array[196, 196, 3] n=115248 (900 KiB) xβ[0. |ββββββββββ| 1.000] ΞΌ=0.361 Ο=0.248
lo(numbers_01).chans

# Make 8 images with progressively higher brightness and stack them 2x2x2.
eight_images = (np.stack([numbers]*8) + np.linspace(-2, 2, 8)[:,None,None,None])
eight_images = (eight_images
*np.array(in_stats[1])
+np.array(in_stats[0])
).clip(0,1).reshape(2,2,2,196,196,3)
lo(eight_images)
array[2, 2, 2, 196, 196, 3] n=921984 (7.03 MiB) xβ[0. |ββββββββββ| 1.000] ΞΌ=0.382 Ο=0.319
lo(eight_images).rgb

lo(numbers+3).plt
lo(numbers+3).plt(center="mean", max_s=1000)
lo(numbers+3).plt(center="range")
from lovely_numpy import set_config, config, lovely
set_config(precision=5, sci_mode=True, color=False)
lo(np.array([1.,2,np.nan]))
array[3] ΞΌ=1.50000e+00 Ο=5.00000e-01 NaN! [1.00000e+00, 2.00000e+00, nan]
set_config(precision=None, sci_mode=None, color=None) # None -> Reset to defaults
lo(np.array([1.,2,np.nan]))
array[3] ΞΌ=1.500 Ο=0.500 NaN! [1.000, 2.000, nan]
# Or with config context manager.
with config(sci_mode=True):
print(lo(np.array([1,2,3])))
print(lo(np.array([1,2,3])))
array[3] i64 xβ[1, 3] ΞΌ=2.000e+00 Ο=8.165e-01 [1, 2, 3]
array[3] i64 xβ[1, 3] ΞΌ=2.000 Ο=0.816 [1, 2, 3]
Lofrom lovely_numpy import rgb, chans, plot
lovely(numbers) # Returns `str`, that's why you see ''.
# Note: lo(x) returns a wrapper object with a `__repr__` and other methods.
'array[196, 196, 3] f32 n=115248 (450 KiB) xβ[-2.118 |ββ
ββββββββ| 2.640] ΞΌ=-0.388 Ο=1.073'
rgb(numbers, denorm=in_stats) # OR: denorm='imagenet'

chans(numbers*0.3+0.5)

plot(numbers)
lo(numbers).rgb(in_stats).fig # matplotlib figure

lo(numbers).plt.fig.savefig('pretty.svg') # Save it
!file pretty.svg; rm pretty.svg
pretty.svg: SVG Scalable Vector Graphics image
fig = plt.figure(figsize=(8,3))
fig.set_constrained_layout(True) # type: ignore
gs = fig.add_gridspec(2,2)
ax1 = fig.add_subplot(gs[0, :])
ax2 = fig.add_subplot(gs[1, 0])
ax3 = fig.add_subplot(gs[1,1:])
ax2.set_axis_off()
ax3.set_axis_off()
lo(numbers_01).plt(ax=ax1)
lo(numbers_01).rgb(ax=ax2)
lo(numbers_01).chans(ax=ax3);

Jupyter Notebook
99.1%
NumPy arrays, ready for human consumption
76
stars
110
commits
Jupyter Notebook
primary language
May 15, 2026
updated
JAXpip install lovely-numpy
or
conda install -c conda-forge lovely-numpy
How often do you find yourself debugging NumPy code? You dump your variable to the cell output, and see this:
numbers
array([[[-0.3541, -0.1975, -0.6715],
[-0.3369, -0.1975, -0.9853],
...,
[-0.4739, -0.3725, -0.689 ],
[ 2.2489, 2.4111, 2.396 ]],
[[-0.4054, -0.25 , -0.7238],
[-0.4226, -0.2325, -1.0724],
...,
[-0.8507, -0.6702, -1.0201],
[ 2.1633, 2.3585, 2.3263]],
...,
[[-0.8507, -0.3901, -1.1944],
[-0.7822, -0.2325, -1.4559],
...,
[-1.5014, -1.2304, -1.4733],
[ 2.1804, 2.4111, 2.4308]],
[[-0.8335, -0.4076, -1.2293],
[-0.8164, -0.285 , -1.5256],
...,
[-1.5528, -1.2829, -1.5256],
[ 2.1119, 2.341 , 2.3611]]], shape=(196, 196, 3), dtype=float32)
Was it really useful for you, as a human, to see all these numbers?
What is the shape? The size?
What are the statistics?
Are any of the values nan or inf?
Is it an image of a man holding a tench?
from lovely_numpy import lo
Lo and behold!lo(numbers)
array[196, 196, 3] f32 n=115248 (450 KiB) xβ[-2.118 |ββ
ββββββββ| 2.640] ΞΌ=-0.388 Ο=1.073
Better, eh?
lo(numbers[1,:6,1]) # Still shows values if there are not too many.
array[6] f32 xβ[-0.408, -0.232] ΞΌ=-0.340 Ο=0.075 [-0.250, -0.232, -0.338, -0.408, -0.408, -0.408]
spicy = numbers[0,:12,0].copy()
spicy[0] *= 10000
spicy[1] /= 10000
spicy[2] = float('inf')
spicy[3] = float('-inf')
spicy[4] = float('nan')
spicy = spicy.reshape((2,6))
lo(spicy) # Spicy stuff
array[2, 6] f32 n=12 xβ[-3.541e+03, -3.369e-05] ΞΌ=-393.776 Ο=1.113e+03 +Inf! -Inf! NaN!
lo(np.zeros((10, 10))) # A zero array - make it obvious
array[10, 10] n=100 all_zeros
lo(spicy, verbose=True)
array[2, 6] f32 n=12 xβ[-3.541e+03, -3.369e-05] ΞΌ=-393.776 Ο=1.113e+03 +Inf! -Inf! NaN!
array([[-3540.5432, -0. , ..., nan, -0.4054],
[ -0.4226, -0.4911, ..., -0.5424, -0.5082]],
shape=(2, 6), dtype=float32)
.deeperlo(numbers.transpose(2,1,0)).deeper
array[3, 196, 196] f32 n=115248 (450 KiB) xβ[-2.118 |ββ
ββββββββ| 2.640] ΞΌ=-0.388 Ο=1.073
array[196, 196] f32 n=38416 xβ[-2.118 |βββ
βββββββ| 2.249] ΞΌ=-0.324 Ο=1.036
array[196, 196] f32 n=38416 xβ[-1.966 |βββ
βββββββ| 2.429] ΞΌ=-0.274 Ο=0.973
array[196, 196] f32 n=38416 xβ[-1.804 |ββββββββββ| 2.640] ΞΌ=-0.567 Ο=1.178
# You can go deeper if you need to
lo(numbers[:3,:4]).deeper(2)
array[3, 4, 3] f32 n=36 xβ[-1.125, -0.197] ΞΌ=-0.563 Ο=0.280
array[4, 3] f32 n=12 xβ[-0.985, -0.197] ΞΌ=-0.487 Ο=0.259
array[3] f32 xβ[-0.672, -0.197] ΞΌ=-0.408 Ο=0.197 [-0.354, -0.197, -0.672]
array[3] f32 xβ[-0.985, -0.197] ΞΌ=-0.507 Ο=0.343 [-0.337, -0.197, -0.985]
array[3] f32 xβ[-0.881, -0.303] ΞΌ=-0.530 Ο=0.252 [-0.405, -0.303, -0.881]
array[3] f32 xβ[-0.776, -0.303] ΞΌ=-0.506 Ο=0.199 [-0.440, -0.303, -0.776]
array[4, 3] f32 n=12 xβ[-1.072, -0.232] ΞΌ=-0.571 Ο=0.281
array[3] f32 xβ[-0.724, -0.250] ΞΌ=-0.460 Ο=0.197 [-0.405, -0.250, -0.724]
array[3] f32 xβ[-1.072, -0.232] ΞΌ=-0.576 Ο=0.360 [-0.423, -0.232, -1.072]
array[3] f32 xβ[-0.968, -0.338] ΞΌ=-0.599 Ο=0.268 [-0.491, -0.338, -0.968]
array[3] f32 xβ[-0.968, -0.408] ΞΌ=-0.651 Ο=0.235 [-0.577, -0.408, -0.968]
array[4, 3] f32 n=12 xβ[-1.125, -0.285] ΞΌ=-0.631 Ο=0.280
array[3] f32 xβ[-0.828, -0.303] ΞΌ=-0.535 Ο=0.219 [-0.474, -0.303, -0.828]
array[3] f32 xβ[-1.125, -0.285] ΞΌ=-0.628 Ο=0.360 [-0.474, -0.285, -1.125]
array[3] f32 xβ[-1.020, -0.390] ΞΌ=-0.651 Ο=0.268 [-0.542, -0.390, -1.020]
array[3] f32 xβ[-1.003, -0.478] ΞΌ=-0.708 Ο=0.219 [-0.645, -0.478, -1.003]
.rgb colorThe important queston - is it our man?
lo(numbers).rgb

Maaaaybe? Looks like someone normalized him.
in_stats = ( (0.485, 0.456, 0.406), # mean
(0.229, 0.224, 0.225) ) # std
# numbers.rgb(in_stats, cl=True) # For channel-last input format
lo(numbers).rgb(denorm=in_stats)
# Denorm is used to convert the input to [0..1], which is then mapped to RGB.
# lo(numbers).rgb(denorm="imagenet") # same as above
# lo(numbers).rgb(denorm="symmetric") # [-1 .. 1] input
# lo(numbers).rgb(denorm="minmax") # Use the min/max elements in each channel to scale the input to [0..1]

Itβs indeed our hero, the Tenchman!
.chans# .chans will map values betwen [-1,1] to colors.
# Make our values fit into that range to avoid clipping.
mean = np.array(in_stats[0])
std = np.array(in_stats[1])
numbers_01 = (numbers*std + mean).clip(0,1)
lo(numbers_01)
array[196, 196, 3] n=115248 (900 KiB) xβ[0. |ββββββββββ| 1.000] ΞΌ=0.361 Ο=0.248
lo(numbers_01).chans

# Make 8 images with progressively higher brightness and stack them 2x2x2.
eight_images = (np.stack([numbers]*8) + np.linspace(-2, 2, 8)[:,None,None,None])
eight_images = (eight_images
*np.array(in_stats[1])
+np.array(in_stats[0])
).clip(0,1).reshape(2,2,2,196,196,3)
lo(eight_images)
array[2, 2, 2, 196, 196, 3] n=921984 (7.03 MiB) xβ[0. |ββββββββββ| 1.000] ΞΌ=0.382 Ο=0.319
lo(eight_images).rgb

lo(numbers+3).plt
lo(numbers+3).plt(center="mean", max_s=1000)
lo(numbers+3).plt(center="range")
from lovely_numpy import set_config, config, lovely
set_config(precision=5, sci_mode=True, color=False)
lo(np.array([1.,2,np.nan]))
array[3] ΞΌ=1.50000e+00 Ο=5.00000e-01 NaN! [1.00000e+00, 2.00000e+00, nan]
set_config(precision=None, sci_mode=None, color=None) # None -> Reset to defaults
lo(np.array([1.,2,np.nan]))
array[3] ΞΌ=1.500 Ο=0.500 NaN! [1.000, 2.000, nan]
# Or with config context manager.
with config(sci_mode=True):
print(lo(np.array([1,2,3])))
print(lo(np.array([1,2,3])))
array[3] i64 xβ[1, 3] ΞΌ=2.000e+00 Ο=8.165e-01 [1, 2, 3]
array[3] i64 xβ[1, 3] ΞΌ=2.000 Ο=0.816 [1, 2, 3]
Lofrom lovely_numpy import rgb, chans, plot
lovely(numbers) # Returns `str`, that's why you see ''.
# Note: lo(x) returns a wrapper object with a `__repr__` and other methods.
'array[196, 196, 3] f32 n=115248 (450 KiB) xβ[-2.118 |ββ
ββββββββ| 2.640] ΞΌ=-0.388 Ο=1.073'
rgb(numbers, denorm=in_stats) # OR: denorm='imagenet'

chans(numbers*0.3+0.5)

plot(numbers)
lo(numbers).rgb(in_stats).fig # matplotlib figure

lo(numbers).plt.fig.savefig('pretty.svg') # Save it
!file pretty.svg; rm pretty.svg
pretty.svg: SVG Scalable Vector Graphics image
fig = plt.figure(figsize=(8,3))
fig.set_constrained_layout(True) # type: ignore
gs = fig.add_gridspec(2,2)
ax1 = fig.add_subplot(gs[0, :])
ax2 = fig.add_subplot(gs[1, 0])
ax3 = fig.add_subplot(gs[1,1:])
ax2.set_axis_off()
ax3.set_axis_off()
lo(numbers_01).plt(ax=ax1)
lo(numbers_01).rgb(ax=ax2)
lo(numbers_01).chans(ax=ax3);

Jupyter Notebook
99.1%