crosszip is a Python library for iterating over all combinations from multiple iterables.
It provides a simple and efficient way to apply functions across combinations of elements.
## Home documentation
### index
# crosszip
[](https://pypi.org/project/crosszip/)

[](https://pypistats.org/packages/crosszip)
`crosszip` is a Python utility that makes it easy to apply a function to
all possible combinations of elements from multiple iterables. It
combines the power of the Cartesian product and functional programming
into a single, intuitive tool.
Additionally, `@pytest.mark.crosszip_parametrize` is a `pytest` marker
that simplifies running tests with all possible combinations of
parameter values.
## Installation
| Package Manager | Installation Command |
|-----------------|------------------------|
| pip | `pip install crosszip` |
| uv | `uv add crosszip` |
## Usage
Example of using `crosszip`:
``` python
# @pyodide
# Label Generation for Machine Learning
from crosszip import crosszip
def create_label(category, subcategory, version):
return f"{category}_{subcategory}_v{version}"
categories = ["cat", "dog"]
subcategories = ["small", "large"]
versions = ["1.0", "2.0"]
labels = crosszip(create_label, categories, subcategories, versions)
print(labels)
```
['cat_small_v1.0', 'cat_small_v2.0', 'cat_large_v1.0', 'cat_large_v2.0', 'dog_small_v1.0', 'dog_small_v2.0', 'dog_large_v1.0', 'dog_large_v2.0']
Example of using `pytest` marker `crosszip_parametrize`:
``` python
# @pyodide
# Testing Power Function
import math
import crosszip
import pytest
@pytest.mark.crosszip_parametrize(
"base",
[2, 10],
"exponent",
[-1, 0, 1],
)
def test_power_function(base, exponent):
result = math.pow(base, exponent)
assert result == base**exponent
print("Tests executed successfully.")
```
Tests executed successfully.
For more examples, check out the package documentation at:
## Key Features
- **Flexible Input**: Works with any iterables, including lists, tuples,
sets, and generators.
- **pytest Plugin**: Provides a `crosszip_parametrize` marker for
running tests with all possible combinations of parameter values.
- **Simple API**: Minimalist, intuitive design for quick integration
into your projects.
- **Advanced patterns**: See the [Advanced Usage](advanced.md) guide for
memory efficiency strategies, managing test explosion, and testing
validation functions across input types and edge cases.
## License
This project is licensed under the MIT License.
## Acknowledgements
Hex sticker font is `Rubik`, and the image is taken from icon made by
Freepik and available at flaticon.com.
## Advanced usage
### advanced
---
tags:
- advanced
- memory
- performance
- testing
- parametrize
---
# Advanced Usage
This guide covers three practical scenarios that the basic examples leave open:
1. Testing a validation function against all combinations of input types and edge cases
2. Handling memory efficiency when working with large iterables
3. Managing test explosion when the Cartesian product grows too large
---
## Testing a Validation Function Across Input Types and Edge Cases
`@pytest.mark.crosszip_parametrize` is well-suited for exhaustively testing a function
against a grid of input types and edge cases. The marker uses alternating
`"param_name", [values]` pairs, and the full Cartesian product becomes the test matrix.
### The validation function under test
```python
def validate_input(value):
"""Accept only non-empty strings and positive numbers."""
if not isinstance(value, (int, float, str)):
raise TypeError(f"Unsupported type: {type(value).__name__}")
if isinstance(value, str) and len(value) == 0:
raise ValueError("String must not be empty")
if isinstance(value, (int, float)) and value <= 0:
raise ValueError("Number must be positive")
return True
```
### Happy-path combinations
The following test generates `len([int, str, float]) × len([1, "hello", 3.14])` = **9 test cases**,
one for each combination of type and a known-valid value:
```python
import pytest
@pytest.mark.crosszip_parametrize(
"input_type",
[int, str, float],
"valid_value",
[1, "hello", 3.14],
)
def test_validate_input_happy_path(input_type, valid_value):
result = validate_input(input_type(valid_value))
assert result is True
```
### Edge-case combinations with expected failures
Stack `@pytest.mark.xfail` above `@pytest.mark.crosszip_parametrize` to mark
all generated test cases as expected failures. The two markers compose correctly
because `crosszip_parametrize` runs through pytest's `metafunc.parametrize` hook
and `xfail` is applied at the test-item level:
```python
import pytest
@pytest.mark.xfail(raises=(TypeError, ValueError), strict=True)
@pytest.mark.crosszip_parametrize(
"input_type",
[int, str, float],
"edge_case",
[None, "", 0],
)
def test_validate_input_edge_cases(input_type, edge_case):
validate_input(edge_case)
```
`strict=True` means any test case that unexpectedly *passes* is itself reported
as a failure — useful for catching regressions where validation logic becomes
too permissive.
### Per-combination assertions with `pytest.raises`
When you need different assertions per combination, pre-compute the expected
exception and pass it as an additional parameter:
```python
import pytest
@pytest.mark.crosszip_parametrize(
"bad_value",
[None, "", 0, -1],
"expected_exc",
[TypeError, ValueError],
)
def test_validate_input_error_types(bad_value, expected_exc):
with pytest.raises((TypeError, ValueError)):
validate_input(bad_value)
```
!!! tip
Use `pytest.raises` when you want to assert the *specific* exception type
per combination. Use `@pytest.mark.xfail` when the entire matrix is expected
to raise and you want a compact, readable declaration.
---
## Memory Efficiency and Large Iterables
### How crosszip allocates memory
`crosszip` is implemented as:
```python
list(itertools.starmap(func, itertools.product(*iterables)))
```
Two materialization points occur:
1. `itertools.product` buffers all input iterables into tuples in memory.
2. The outer `list()` call holds every return value of `func` simultaneously.
The total number of combinations follows the formula:
```
total_combinations = N₁ × N₂ × … × Nₖ
```
You can estimate this before calling `crosszip`:
```python
import math
sizes = [100, 50, 20, 10]
total = math.prod(sizes)
print(f"Total combinations: {total:,}") # 1,000,000
```
!!! warning "Exponential growth"
With 5 iterables of 100 elements each, `crosszip` must hold
10,000,000,000 result objects in memory simultaneously.
For large inputs, use a lazy approach instead.
### The lazy workaround: `itertools.product` as a generator
Since `crosszip` always returns a `list`, the only way to process combinations
one at a time — without loading all results — is to use `itertools.product`
directly in a generator function:
```python
import itertools
def process_lazily(func, *iterables):
"""Generator version of crosszip — yields one result at a time."""
for combo in itertools.product(*iterables):
yield func(*combo)
# Consume without materializing the full result list
large_a = range(1_000)
large_b = range(1_000)
for result in process_lazily(lambda a, b: a + b, large_a, large_b):
# Each result is produced and can be discarded before the next is computed.
pass
```
!!! note
`itertools.product` still buffers the *input* iterables into tuples
internally, so generator inputs are exhausted on the first call.
Only the *output* stream is lazy.
### Chunk processing
For pipelines that need batches rather than individual results — writing to
disk, sending over a network, etc.:
```python
import itertools
def chunked_crosszip(func, chunk_size, *iterables):
"""Process combinations in fixed-size chunks."""
combo_iter = itertools.product(*iterables)
while True:
chunk = list(itertools.islice(combo_iter, chunk_size))
if not chunk:
break
yield [func(*combo) for combo in chunk]
# Process 1 000 combinations at a time
for batch in chunked_crosszip(str, 1_000, range(500), range(500)):
# Flush batch to disk before fetching the next one
pass
```
### Guard pattern: fail fast on oversized inputs
Add a pre-flight check using `math.prod` to raise a clear error before
`crosszip` silently consumes all available memory:
```python
import math
from crosszip import crosszip
def safe_crosszip(func, *iterables, limit: int = 10_000):
"""Call crosszip only if combination count is within the limit."""
# Convert to lists so we can measure length without exhausting generators
lists = [list(it) for it in iterables]
total = math.prod(len(lst) for lst in lists)
if total > limit:
raise ValueError(
f"Combination count {total:,} exceeds limit {limit:,}. "
"Reduce input sizes or use a lazy approach."
)
return crosszip(func, *lists)
```
---
## Managing Test Explosion
### Understanding combinatorial growth
The number of test cases grows as the product of all value-list lengths:
| Parameters | Values each | Test cases |
|:----------:|:-----------:|:----------:|
| 2 | 3 | 9 |
| 3 | 4 | 64 |
| 4 | 5 | 625 |
| 5 | 10 | 100,000 |
| 6 | 10 | 1,000,000 |
A realistic example that quietly produces 64 test cases:
```python
import pytest
@pytest.mark.crosszip_parametrize(
"protocol", ["http", "https", "ftp", "ssh"],
"method", ["GET", "POST", "PUT", "DELETE"],
"auth_type", ["none", "basic", "token", "oauth"],
)
def test_api_client(protocol, method, auth_type):
... # 4 × 4 × 4 = 64 test cases
```
### Strategy 1: Pre-filter combinations
`crosszip_parametrize` always generates the full Cartesian product. When only a
subset of combinations is meaningful, pre-filter with `itertools.product` and
pass the result to the standard `pytest.mark.parametrize`:
```python
import itertools
import pytest
protocols = ["http", "https", "ftp", "ssh"]
methods = ["GET", "POST", "PUT", "DELETE"]
# Only test secure protocols with write methods
meaningful_combos = [
(proto, method)
for proto, method in itertools.product(protocols, methods)
if proto == "https" or method == "GET"
]
# Reduces 4 × 4 = 16 → 7 combinations
@pytest.mark.parametrize("protocol,method", meaningful_combos)
def test_secure_api(protocol, method):
...
```
### Strategy 2: Use `-k` for focused runs during development
pytest's `-k` flag matches test IDs by substring, letting you run a slice of
the generated matrix without changing any code:
```bash
# Run only the https combinations
pytest -k "https"
# Run only POST combinations
pytest -k "POST"
# Narrow further with boolean operators
pytest -k "https and POST"
```
### Strategy 3: Random sampling with a fixed seed
Statistical coverage without full enumeration — and deterministic enough for CI:
```python
import itertools
import random
import pytest
protocols = ["http", "https", "ftp", "ssh"]
methods = ["GET", "POST", "PUT", "DELETE"]
all_combos = list(itertools.product(protocols, methods))
SEED = 42 # fix the seed so CI runs are reproducible
sampled = random.Random(SEED).sample(all_combos, k=min(5, len(all_combos)))
@pytest.mark.parametrize("protocol,method", sampled)
def test_api_sample(protocol, method):
...
```
### Strategy 4: Split into focused test classes
Divide a large parameter space into smaller, purpose-scoped classes so each
runs only the combinations relevant to its concern:
```python
import pytest
class TestCoreProtocols:
@pytest.mark.crosszip_parametrize(
"method", ["GET", "POST"],
"auth", ["none", "basic"],
)
def test_http_methods(self, method, auth):
... # 2 × 2 = 4 tests
class TestSecureProtocols:
@pytest.mark.crosszip_parametrize(
"method", ["GET", "POST", "PUT", "DELETE"],
"token", ["valid", "expired"],
)
def test_https_methods(self, method, token):
... # 4 × 2 = 8 tests
```
### Strategy 5: Tiered test suite with custom marks
Keep a fast smoke test always active and gate the full combinatorial matrix
behind a `slow` mark, run only in nightly CI:
```python
import pytest
# Always runs — representative single combination
def test_api_smoke():
...
# Skipped in fast mode, run on schedule
@pytest.mark.slow
@pytest.mark.crosszip_parametrize(
"protocol", ["http", "https", "ftp", "ssh"],
"method", ["GET", "POST", "PUT", "DELETE"],
)
def test_api_full_matrix(protocol, method):
... # 4 × 4 = 16 tests, guarded by the slow mark
```
Register the mark in `pyproject.toml` to silence the unknown-mark warning:
```toml
[tool.pytest.ini_options]
markers = [
"slow: marks tests as slow (deselect with '-m \"not slow\"')",
]
```
Run selectively:
```bash
# Fast CI run — skip the full matrix
pytest -m "not slow"
# Nightly run — include everything
pytest -m "slow"
```
## API documentation
### crosszip
# crosszip
::: crosszip.crosszip
### plugin
# pytest-plugin: crosszip_parametrize
::: crosszip.plugin
## Changelog
### changelog
# Changelog
## 1.3.0
- Adds support for Python version `3.14`.
## 1.2.0
- No user-facing changes.
## 1.1.0
- Extends support to Python versions `3.10` and `3.11`.
## 1.0.0
- Adds pytester tests for the `pytest`-plugin.
## 0.2.0
- Fixes `crosszip_parametrize` marker for `pytest` plugin. There was a bug in the implementation that caused the marker to not be recognized by `pytest`.
## 0.1.0
- Initial release of `crosszip` package.