Skip to main content
Threaded Python

Threadful

Python Threads - from Dreadful to Threadful

Installation

pip install threadful

Usage

Example 1: Basic usage of @thread

from threadful import thread


@thread  # with or without ()
def some_function():
    time.sleep(10)
    return " done "


# when ready, it will call these callback functions.
some_function().then(lambda result: result.strip()).then(lambda result: print(result))  # prints: "done"

promise = some_function()  # ThreadWithResult[str] object
promise.result()  # Err(None)
time.sleep(15)  # after the thread is done:
promise.result()  # Ok(" done ")

# alternative to sleep:
result = promise.join()  # " done " if success, raises if the thread raised an exception

What's happening:

  • The @thread decorator wraps some_function to make it run in a separate thread when invoked.
  • Calling some_function() doesn't start the thread immediately. Instead, it returns a ThreadWithResult object, allowing you to attach callbacks using .then() or .catch() before the thread starts.
  • The thread starts running when you begin checking for the result (result, join) or explicitly start it.
  • The .then() chain demonstrates how to process the result when the thread completes.
  • The promise.result() method gives access to the result (Ok or Err) if available.
  • The .join() method blocks until the thread finishes and directly returns the result or raises any exception from the thread.

Example 2: Handling exceptions in threads

@thread()
def raises() -> str:
    raise ValueError()


promise = raises().catch(lambda err: TypeError())

promise.join()  # raises TypeError
promise.result()  # Err(TypeError)


promise = raises().catch(lambda err: "Something went wrong")

promise.join()  # returns the string "Something went wrong"

What's happening:

  • The @thread decorator is used on raises(), which deliberately raises a ValueError.
  • The first catch() replaces the ValueError with a TypeError, so calling promise.join() raises the new TypeError.
  • The second catch() provides a fallback string "Something went wrong". When the thread completes, calling join() returns this string instead of raising an exception.
  • This mechanism allows you to gracefully handle errors in the thread without crashing your program.

Example: Animating a Function with Different Options

from threadful import thread, animate
import time


@thread
def wait(duration: int):
    time.sleep(duration)
    return f"Waited for {duration} seconds"


# Example 1: Basic animation with static text
result = animate(wait(3), text="Waiting...")  # Output: "Waited for 3 seconds"

# Example 2: Threaded animation (non-blocking)
thread_result = animate(wait(3), text="Running asynchronously...", threaded=True)
# Animation running in the background...
# you can do other things in the main thread here
thread_result.join()  # Get the result (str) after the thread completes

# Example 3: Dynamic text animation
animate(wait(3), text=lambda: f"Current time: {time.strftime('%H:%M:%S')}")
# Animation with dynamic text complete!

What's Happening:

  1. Basic Animation: Runs wait with a loading message ("Waiting...") and returns the result after completion.
  2. Threaded Animation: Runs both wait and the animation in the background. Use .join() to get the result.
  3. Dynamic Text: Updates the animation text dynamically using a callback (e.g., current time).

These examples show how to use animate for different scenarios.

Non-interactive output (CI, pipes, redirects):

The animation writes to stderr using carriage returns, which only makes sense on a terminal. Most CI log viewers treat a carriage return as a line break, so an animation would add a log line for every frame (20 per second by default).

animate therefore detects whether stderr is a terminal. When it isn't, it writes plain log lines ending in a newline, and only when the text actually changed:

  • static text produces a single line;
  • a callback produces one line per distinct value, so text=lambda: f"{done}/{total}" gives you real progress in your build log;
  • clear_with adds one final line, prefixed with that marker;
  • no text means no output at all, and the cursor escape codes are skipped.

Pass force_animation=True or force_animation=False to override the detection.


Important Note:

A thread doesn't start running immediately when you invoke the decorated function (e.g., some_function()). This delay allows you to attach callbacks (then, catch) before the thread begins execution. The thread starts:

  1. When you check for its result (result).
  2. When you block for its completion (join).
  3. In the background if you explicitly call .start().

License

threadful is distributed under the terms of the MIT license.

Changelog

See CHANGELOG.md

Metadata

Release files for threadful 0.6.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for threadful 0.6.1
File Size Uploaded
threadful-0.6.1.tar.gz 10.3 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for threadful 0.6.1
File Interpreter ABI Platform
threadful-0.6.1-py3-none-any.whl Python 3 none any Details

Total release size: 20.1 kB

Release files / threadful-0.6.1.tar.gz

Download URL threadful-0.6.1.tar.gz
Size 10.3 kB
Tags Source
SHA-256 checksum
How to use checksums
2411a06f2947f42d15b57362240a3f0c5e10984807065e3dd3ca78836e494d81
BLAKE2b-256 checksum
How to use checksums
16baca2cc22a30ed7714ba6a1c920897d4235ca880bc0c4734526254bad3c6c9
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.12.11 {"installer":{"name":"uv","version":"0.12.11","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Linux Mint","version":"22.3","id":"zena","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release files / threadful-0.6.1-py3-none-any.whl

Download URL threadful-0.6.1-py3-none-any.whl
Size 9.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
c9220a2da5548511ac1d53edea01f4eff6b137a9781360dd4c80151f7f183705
BLAKE2b-256 checksum
How to use checksums
3535be5addfb977f1a6c5147aaf0c0fa620e122e8c5ef6886448cc7357c408d5
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.12.11 {"installer":{"name":"uv","version":"0.12.11","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Linux Mint","version":"22.3","id":"zena","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release history Release notifications | RSS feed

This release

0.6.1 This release

2 release files

0.6.0

2 release files

0.5.1

2 release files

0.5.0

2 release files

0.4.0

2 release files

0.3.0

2 release files

0.2.2

2 release files

0.2.1

2 release files

0.2.0

2 release files

0.1.1

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page