Chaining function calls on steroids
Project description
threadx - Create elegant data transformation pipelines. It lets you thread values through a sequence of operations with a sense of clarity and simplicity that feels natural. And it all revolves around two key elements:
- thread: Passes the result of each step as the input to the next.
- x: A smart placeholder that knows exactly where to inject the previous result, whether in a method call, item lookup, or even unpacking.
Here’s what it looks like in action:
from threadx import xf, x
xf('./data.log',
read_file,
x.splitlines(),
(map, x.strip(), x),
(map, json.loads, x),
(map, x['time'], x),
sum)
# or
xl('./data.log',
read_file,
x.splitlines(),
(map, x.strip()),
(map, json.loads),
(map, x['time']),
sum)
What’s happening here? The file content is being read, split, stripped, converted to JSON, and the execution-time summed—all in a linear and readable way. No intermediary variables, no nesting, just the data flowing from one step to the next.
The data.log file (generated by inspector) contains entries like this:
{"time": 12000, "fn": "foo", ...}
{"time": 12345, "fn": "bar", ...}
What Makes threadx Interesting?
- Readable Flow: Instead of diving into layers of nested calls, you write each transformation as a clear, sequential step.
- The
xFactor:xacts as a placeholder for where the output of the previous step goes. It’s surprisingly flexible, supporting method calls, attribute/item lookups, and more. - No Extra Variables: Avoid the noise of intermediate variables or lambda functions. Your transformations stay clean and minimal.
Table of Contents
Install
pip install threadx
Usage
Import
from threadx import xf, xl, x
Pass result as first argument
xf allows you to pass the result of the previous step automatically as the first argument in each new function:
xf([1, 2, 3], # => [1, 2, 3]
sum, # => 6
str) # => '6'
Or, be explicit about it:
xf([1, 2, 3],
(sum, x),
(str, x))
Pass x as nth argument
Want to pass the result into a different argument position? No problem:
xf(10,
(range, x, 20, 3), # same as (range, 20, 3)
list) # => [10, 13, 16, 19]
xf(20,
(range, 10, x, 3),
list) # => [10, 13, 16, 19]
xf(3,
(range, 10, 20, x),
list) # => [10, 13, 16, 19]
Pass x as last argument
xl works same as xf, with just one change, that x will be passed as the last argument.
- xl - pass x as last
- xf - pass x as first
Unpacking arguments
Unpacking works as usual
xf([10, 20],
(range, *x, 3), # unpack to (range, 10, 20, 3)
list) # => [10, 13, 16, 19]
Getting Item And Slicing
data = {'a': {'b': [1, 2, 3, 4]}}
xf(data,
x['a'],
x['b'][0]) # => 1
xf(data,
x['a']['b'][:2]) # => [1, 2]
Attribute lookup
Use x.attribute_name to lookup class and instance attributes.
xf(3 + 4j,
x.real) # => 3
xf(3 + 4j,
(x.real)) # => 3
Method call
Use x.method_name() or x.method_name(args) for method calls, just like magic.
data = {'a': 1, 'b': 2}
xf(data,
x.keys(), # same as (x.keys())
list) # => ['a', 'b']
xf(data,
(x.keys()),
(list)) # => ['a', 'b']
xf(data,
x.get('c', 'Not Found')) # => 'Not Found'
Fewer lambdas
Remove verbose lambdas in simple cases.
data = [[1, 2, 3, 4], [10, 20, 30, 40]]
# Normal way:
xf(data,
(map, lambda i: i[0], x),
list) # => [1, 10]
# or
xf(data,
(map, x[0], x),
list) # => [1, 10]
# or
xl(data,
(map, x[0]),
list) # => [1, 10]
# Normal way:
xf(range(12),
(filter, lambda i: i % 2 == 0, x),
list) # => [0, 2, 4, 6, 8, 10]
# or
xf(range(12),
(filter, x % 2 == 0, x),
list) # => [0, 2, 4, 6, 8, 10]
# or
xl(range(12),
(filter, x % 2 == 0),
list) # => [0, 2, 4, 6, 8, 10]
Build data transformation pipeline
# make a tuple or list
pipeline = (read_file,
x.splitlines(),
(map, x.strip()),
(map, json.loads),
(map, x['time']),
sum)
xl('./data.log', *pipeline) # works jsut like any other function.
Blowing your brain
# Not saying to solve this problem this way,
# Just showing what `x` can do
answer_sheet = [{'a': 1, 'b': 2, 'op': op.add , 'marks': 1, 'answer': 3},
{'a': 1, 'b': 2, 'op': op.mul , 'marks': 2, 'answer': 2},
{'a': 1, 'b': 2, 'op': op.truediv, 'marks': 2, 'answer': 0.6} # <- Incorrect answer by student
]
# need 60% to pass
passing_marks = 3
# note it is not a lambda
correct_answer = x['op'](x['a'], x['b']) == x['answer']
xl(answer_sheet,
(filter, correct_answer), # <--
(map, x['marks']),
(sum),
(x >= passing_marks) # <--
) # => True
Why I Built This
After spending a few years working with Clojure, I found myself missing its threading macros when I returned to Python (for a side project). Sure, Python has some tools for chaining operations, but nothing quite as elegant or powerful as what I was used to.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file threadx-0.1.0a3.tar.gz.
File metadata
- Download URL: threadx-0.1.0a3.tar.gz
- Upload date:
- Size: 12.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.12.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
08fb89fa88a60512b21dee3af238fbafbf9cfd6ffa4c3ce170aa12b3562147cb
|
|
| MD5 |
dcef95d1ae0733e2610111aa41a6ba99
|
|
| BLAKE2b-256 |
e9353d4d2291353b610191977fadbc82b1b579bde1520ab55bf38c066585b32c
|
File details
Details for the file threadx-0.1.0a3-py3-none-any.whl.
File metadata
- Download URL: threadx-0.1.0a3-py3-none-any.whl
- Upload date:
- Size: 14.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.12.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
86fc468bccd31a131ebfb0a7f644a3976d2079a66858c4a63c5164fad2a6a67f
|
|
| MD5 |
a6de44fd9bf1cd19dc416a14438c0976
|
|
| BLAKE2b-256 |
c4eb54461a79fbed0e750a8dcb814652ce710123b7ca225c59f8a41e258392bb
|