Miscellaneous

Timer

Use timer as a standalone class so you have full control on when to call a lap (most useful in while loops)…

N = 50
t = Timer(N)
info = None

for i in range(N):
    time.sleep(0.01)
    t(info=info)  # Lap and present the time
    if i == N // 2:
        print()
        info = f"My Info: {i*3.122}"
26/50 (0.32s  - 0.30s remaining - 81.30 iters/s)          
My Info: 78.05  50/50 (0.61s  - 0.00s remaining - 81.75 iters/s)          

Track2

… or use track2 to directly track a for loop

N = 50
info = None

for i in (tracker := track2(range(N), total=N)):
    time.sleep(0.01)
    info = f"My Info: {i*3.122:.2f}"
    if i == N // 2:
        print()
    if i >= N // 2:
        tracker.send(info)
25/50 (0.31s  - 0.31s remaining - 81.55 iters/s)          
My Info: 152.98 50/50 (0.61s  - 0.00s remaining - 82.52 iters/s)          

Warning! NEVER RUN tracker.send(None) as this will skip variables silently

Try Catch with a single line

@tryy
def do(a, b, c):
    return 1 / 0


x = do(1, 2, c=10)
assert x is None  # tryy returns None by default
[01/02/25 20:59:38] WARNING  Error for `do`: ZeroDivisionError: division by zero                                                                     579939537.py:wrapper:27

Use your own default on failure

@tryy(output_to_return_on_fail="😔")
def do(a, b, c):
    return 1 / 0


do(1, 2, c=10)
                    WARNING  Error for `do`: ZeroDivisionError: division by zero                                                                     579939537.py:wrapper:27
'😔'

Optionally print the full stacktrace if needed

@tryy(print_traceback=True, output_to_return_on_fail="😔")
def do(a, b, c):
    return 1 / 0


do(1, 2, c=10)
                    WARNING  Error for `do`:                                                                                                         579939537.py:wrapper:32
                             Traceback (most recent call last):                                                                                                             
                               File "/var/folders/1_/71dqv9vx2750gmyz77q_f45w0000gn/T/ipykernel_70802/579939537.py", line 22, in wrapper                                    
                                 return f(*args, **kwargs)                                                                                                                  
                                        ^^^^^^^^^^^^^^^^^^                                                                                                                  
                               File "/var/folders/1_/71dqv9vx2750gmyz77q_f45w0000gn/T/ipykernel_70802/580638143.py", line 3, in do                                          
                                 return 1 / 0                                                                                                                               
                                        ~~^~~                                                                                                                               
                             ZeroDivisionError: division by zero                                                                                                            
                                                                                                                                                                            
'😔'

You can also silence the errors completely

@tryy(silence_errors=True, output_to_return_on_fail="😔")
def do(a, b, c):
    return 1 / 0


do(1, 2, c=10)
'😔'

You can collect all your errors in a list

import random

errors = []


@tryy(silence_errors=True, store_errors=errors)
def do(a, b, c):
    if random.randint(0, 100) < 50:
        return 1 / 0
    else:
        raise NotImplementedError("🤔")


for _ in range(4):
    do(1, random.randint(0, 10), c=random.randint(0, 100))

print(errors)
[{'func': 'do', 'args': (1, 4), 'kwargs': {'c': 53}, 'tb': None, 'err_type': 'ZeroDivisionError'}, {'func': 'do', 'args': (1, 3), 'kwargs': {'c': 68}, 'tb': None, 'err_type': 'ZeroDivisionError'}, {'func': 'do', 'args': (1, 1), 'kwargs': {'c': 2}, 'tb': None, 'err_type': 'NotImplementedError'}, {'func': 'do', 'args': (1, 10), 'kwargs': {'c': 96}, 'tb': None, 'err_type': 'NotImplementedError'}]

There’s onlly one usecase where you would want to send in a list by yourself - when you want to append your errors to an existing list. The sensible default is to always store the errors, especially because this is a debugging tool.

Just access all the errors in a dataframe like so

import random

random.seed(10)


@tryy(silence_errors=True)
def do(a, b, c):
    if c < 50:
        return 1 / 0
    else:
        raise NotImplementedError("🤔")


for _ in range(4):
    do(1, random.randint(0, 10), c=random.randint(0, 100))

do.error_summary()
func args kwargs tb err_type
0 do (1, 9) {'c': 4} None ZeroDivisionError
1 do (1, 6) {'c': 61} None NotImplementedError
2 do (1, 9) {'c': 1} None ZeroDivisionError
3 do (1, 3) {'c': 59} None NotImplementedError

and the actual list of errors like so

do.error_store
[{'func': 'do',
  'args': (1, 9),
  'kwargs': {'c': 4},
  'tb': None,
  'err_type': 'ZeroDivisionError'},
 {'func': 'do',
  'args': (1, 6),
  'kwargs': {'c': 61},
  'tb': None,
  'err_type': 'NotImplementedError'},
 {'func': 'do',
  'args': (1, 9),
  'kwargs': {'c': 1},
  'tb': None,
  'err_type': 'ZeroDivisionError'},
 {'func': 'do',
  'args': (1, 3),
  'kwargs': {'c': 59},
  'tb': None,
  'err_type': 'NotImplementedError'}]

Finally, you want to run the function (without try) to reproduce the error and actually start debugging. Just use the .F attribute to access the original function that you created

ix = 2
data = do.error_store[ix]
try:
    do.F(*data.args, **data.kwargs)
except Exception as e:
    print(f"ERROR: ", e)
ERROR:  'dict' object has no attribute 'args'


cast_inputs

 cast_inputs (func)
@cast_inputs
def do(a: int, b: int):
    return a + b

assert do(1, 2) == 3
assert do(1, '2') == 3
assert do('1', '2') == 3