This guide covers Python-specific development for the geodatafusion Python package, which provides Python bindings for the Rust geodatafusion library using PyO3.
This guide is in addition to the top-level DEVELOP.md that covers Rust development. You should read that first and all Rust-related instructions also apply here.
The Python package is a separate workspace that wraps the Rust library using:
- PyO3 - Rust bindings for Python (automatically installed via Cargo)
- Maturin - Build system for PyO3 packages (automatically installed in the dev environment by uv)
- uv - Python package and virtual environment manager
uv is the recommended package manager (install instructions).
python/
├── Cargo.toml # Rust package configuration
├── pyproject.toml # Python package configuration
├── src/ # Rust source (PyO3 bindings)
│ ├── lib.rs # Main module entry point
│ ├── registration.rs # FFI UDF registration exports
│ ├── udf/ # UDF registration modules
│ └── utils.rs # Helper utilities
├── python/ # Pure Python source
│ └── geodatafusion/
│ └── __init__.py # Python API
└── tests/ # Python tests
└── udf/ # UDF tests
# From the repository root
cd python
# Create virtual environment and install dependencies
uv sync --no-install-package geodatafusionThe --no-install-package geodatafusion avoids building geodatafusion itself (in release mode) during setup. Maturin is automatically installed in the dev environment by uv.
There are two ways to build the package:
# Build and install in development mode
uv run --no-project maturin develop --uvNote: Debug builds will show a performance warning at runtime. This is expected during development.
# Build optimized release version
uv run --no-project maturin develop --uv --release
# Or build wheel for distribution
uv run --no-project maturin build --uv --release# Run all tests
uv run --no-project pytest
# Run specific test file
uv run --no-project pytest tests/test_register.py
# Run with verbose output
uv run --no-project pytest -v
# Run with output capture disabled (see print statements)
uv run --no-project pytest -sWhen a new UDF is added to the Rust library, you need to expose it in Python:
Follow the instructions in the top-level DEVELOP.md to implement the UDF in Rust first.
The UDF modules are in src/udf/. Each module corresponds to a category:
src/udf/native/- Native implementationssrc/udf/geo/- Geo trait implementationssrc/udf/geohash/- GeoHash functions
Wrap the UDF, using one of our existing macros, if possible.
impl_udf!: for UDFs without any instantiation argumentsimpl_udf_coord_type_arg!: for UDFs taking aCoordTypeargument upon instantiation
For example, in src/udf/geo/mod.rs, register the new function:
#[pymodule]
pub(crate) fn geo(m: &Bound<PyModule>) -> PyResult<()> {
m.add_class::<NewUdf>()?;register_all() exports the scalar and aggregate UDFs from the Rust
geodatafusion::register() registry through DataFusion FFI capsules. Category
registration functions use the corresponding Rust module registrars. Adding a
UDF to an existing Rust module therefore makes it available to Python without
maintaining another name list.
Add or update a Python wrapper class only when users need to construct the UDF
with non-default options, such as coord_type. When adding or updating a
wrapper class in src/udf/, also update the corresponding .pyi type stub
under python/geodatafusion/.
Create tests in tests/udf/ following the existing structure:
from datafusion import SessionContext
from geodatafusion import register_all
def test_my_new_function():
ctx = SessionContext()
register_all(ctx)
sql = "SELECT my_new_function(ST_GeomFromText('POINT(1 2)'));"
result = ctx.sql(sql)
assert result.to_arrow_table().columns[0][0].as_py() == expected_valuePush a new git tag to GitHub, and the CI workflow will build and publish wheels automatically.