py-udp
High-performance UDP networking library for Python with Rust backend.
Architecture
This project uses PyO3 to create a Python extension module from Rust code:
- Rust Implementation - Core UDP functionality implemented in Rust using Tokio
- PyO3 Bindings - Python bindings created with PyO3
- Python API - User-friendly Python interface
Why This Architecture?
- Performance: Core networking implemented in Rust for maximum performance
- Safety: Rust's memory safety and zero-cost abstractions
- Integration: Seamless Python integration through PyO3
- Modern: Uses Tokio for efficient async I/O
Features
- High Performance: Core networking implemented in Rust for maximum performance
- Easy to Use: Simple Python API for UDP server and client operations
- Async Support: Built on Tokio runtime for efficient async I/O
- Type Safe: Full type hints and error handling
- Cross Platform: Works on Windows, macOS, and Linux
- Message Handlers: Flexible callback system for processing incoming messages
- Thread Safety: Safe concurrent access with proper synchronization
Installation
Prerequisites
- Python 3.12+
- Rust (latest stable version)
- Cargo (comes with Rust)
- uv (recommended package manager)
Build and Install
- Clone the repository:
git clone <repository-url>
cd py-udp
- Build and install the package:
uv run maturin develop
Alternative: Using the Build Script
uv run python build.py
This script will:
- Check prerequisites
- Install maturin
- Build the Rust extension
- Install the Python package
- Run tests
Quick Start
UDP Server
from py_udp import UdpServer, MessageHandler
class MyHandler(MessageHandler):
def __call__(self, data: bytes, source_address: str):
print(f"Received from {source_address}: {data.decode()}")
# Process the message...
# Create and start server
server = UdpServer(host="127.0.0.1", port=8888)
server.bind()
server.set_message_handler(MyHandler())
server.start()
# Keep server running
import time
while server.is_running():
time.sleep(1)
UDP Client
from py_udp import UdpClient
# Create client
client = UdpClient()
client.bind()
# Send message
message = "Hello, UDP!".encode('utf-8')
bytes_sent = client.send_to(message, "127.0.0.1", 8888)
print(f"Sent {bytes_sent} bytes")
# Receive response
data, source = client.recv_from()
print(f"Received from {source}: {data.decode()}")
Examples
Echo Server
The echo server example demonstrates how to create a UDP server that responds to incoming messages:
#!/usr/bin/env python3
"""
UDP Server Example
This example demonstrates how to create a UDP server that echoes back
received messages with additional information.
"""
import time
from py_udp import UdpServer, MessageHandler
class EchoHandler(MessageHandler):
"""Echo handler that responds to incoming messages."""
def __init__(self, server: UdpServer):
self.server = server
self.message_count = 0
def __call__(self, data, source_address: str) -> None:
"""Handle incoming message and send response."""
# Convert data to bytes if it's a list
if isinstance(data, list):
data = bytes(data)
self.message_count += 1
# Decode message
try:
message = data.decode('utf-8')
except UnicodeDecodeError:
message = f"<binary data: {len(data)} bytes>"
print(f"[{self.message_count}] Received from {source_address}: {message}")
# Create response
response = f"Echo #{self.message_count}: {message}"
response_data = response.encode('utf-8')
# Send response using the server's send_to method
try:
bytes_sent = self.server.send_to(response_data, source_address)
print(f"Sent {bytes_sent} bytes to {source_address}")
except Exception as e:
print(f"Error sending response: {e}")
def main():
"""Run UDP echo server."""
print("Starting UDP Echo Server...")
# Create server
server = UdpServer(host="127.0.0.1", port=8888)
# Bind to address
server.bind()
print(f"Server bound to {server.address}")
# Set message handler
handler = EchoHandler(server)
server.set_message_handler(handler)
# Start server
server.start()
print("Server started. Press Ctrl+C to stop.")
try:
# Keep server running
while server.is_running():
time.sleep(1)
except KeyboardInterrupt:
print("\nStopping server...")
server.stop()
print("Server stopped.")
if __name__ == "__main__":
main()
Run the echo server example:
uv run python examples/server_example.py
Echo Client
The echo client example demonstrates how to create a UDP client that sends messages and receives responses:
#!/usr/bin/env python3
"""
UDP Client Example
This example demonstrates how to create a UDP client that sends messages
to a server and receives responses.
"""
import time
import threading
from py_udp import UdpClient
def receive_messages(client: UdpClient):
"""Receive messages in a separate thread."""
print("Starting message receiver...")
while True:
try:
# Add a small delay to avoid busy waiting
time.sleep(0.1)
data, source = client.recv_from()
# Convert data to bytes if it's a list
if isinstance(data, list):
data = bytes(data)
message = data.decode('utf-8')
print(f"Received from {source}: {message}")
except Exception as e:
# Don't break on timeout errors
if "timeout" not in str(e).lower():
print(f"Error receiving message: {e}")
break
def main():
"""Run UDP client."""
print("Starting UDP Client...")
# Create client
client = UdpClient(host="127.0.0.1", port=0)
# Bind to random port
client.bind()
print(f"Client bound to {client.address}")
# Start receiver thread
receiver_thread = threading.Thread(
target=receive_messages,
args=(client,),
daemon=True
)
receiver_thread.start()
# Server address
server_host = "127.0.0.1"
server_port = 8888
print(f"Connecting to server at {server_host}:{server_port}")
print("Sending messages automatically...")
try:
message_count = 0
while True:
message_count += 1
# Create test message
message = f"Hello, UDP! #{message_count}"
data = message.encode('utf-8')
# Send message
try:
bytes_sent = client.send_to(data, server_host, server_port)
print(f"Sent: {message} ({bytes_sent} bytes)")
except Exception as e:
print(f"Error sending message: {e}")
# Wait before sending next message
time.sleep(2)
except KeyboardInterrupt:
print("\nStopping client...")
print("Client stopped.")
if __name__ == "__main__":
main()
In another terminal, run the client:
uv run python examples/client_example.py
The client will automatically send messages every 2 seconds and display received responses.
Development
Project Structure
py-udp/
├── src/ # Rust source code
│ └── lib.rs # Main Rust implementation
├── py_udp/ # Python package
│ ├── __init__.py # Python API
│ └── ...
├── examples/ # Usage examples
│ ├── server_example.py # Echo server example
│ └── client_example.py # Echo client example
├── tests/ # Python tests
├── Cargo.toml # Rust project config
├── pyproject.toml # Python package config
└── README.md
Working with Rust Code
# Build Rust extension
uv run maturin develop
# Run Rust tests
cargo test
# Run specific test suites
cargo test --test test_udp # Unit tests
cargo test --test integration_udp # Integration tests
# Run with debug output
RUST_LOG=debug cargo test
# Check code coverage
cargo tarpaulin --skip-clean
Working with Python Package
# Install in development mode
uv run maturin develop
# Run Python tests
uv run pytest tests/
# Run tests with coverage
uv run pytest tests/ --cov=py_udp --cov-report=term-missing
# Run specific test classes
uv run pytest tests/ -k TestUdpServer
uv run pytest tests/ -k TestUdpClient
# Format code
uv run black .
uv run isort .
Running Tests
Rust Tests
# Run all Rust tests (unit + integration)
cargo test
# Run only unit tests
cargo test --lib
# Run only integration tests
cargo test --test integration_udp
cargo test --test test_udp
# Run with debug output
RUST_LOG=debug cargo test
Rust Test Coverage:
- Unit Tests: 19 tests in
tests/test_udp.rs - Integration Tests: 10 tests in
tests/integration_udp.rs - Coverage: 33.08% (44/133 lines covered)
Python Tests
# Run Python tests
uv run pytest tests/
# Run with coverage report
uv run pytest tests/ --cov=py_udp --cov-report=term-missing
# Run with verbose output
uv run pytest tests/ -v
Python Test Coverage:
- Total Tests: 26 tests
- Coverage: 91% (83/91 lines covered)
- Missing Lines: 15-16, 79, 109, 127-130, 188
Test Structure
Rust Tests (tests/ directory):
test_udp.rs- Unit tests for core Rust functionality- Server/client creation and initialization
- Binding and address validation
- Error handling and edge cases
- Basic UDP operations
integration_udp.rs- Integration tests for UDP communication- Client-server echo communication
- Invalid address handling
- Multiple message sending
- Server lifecycle management
Python Tests (tests/test_udp.py):
TestMessageHandler- Message handler base class testsTestUdpServer- Server functionality testsTestUdpClient- Client functionality testsTestConvenienceFunctions- Utility function testsTestUdpCommunication- End-to-end communication tests
Coverage Analysis
Rust Coverage (33.08%):
- Covered: Basic initialization, binding, error handling
- Uncovered: Python GIL-dependent methods, async server logic, PyO3 wrappers
- Limitations: Python interpreter not available in Rust-only tests
Python Coverage (91%):
- Covered: All public API methods, error handling, communication
- Missing: Some edge cases and error conditions
- Strength: Full integration testing with real UDP communication
Coverage Commands
# Rust coverage with cargo-tarpaulin
cargo install cargo-tarpaulin
cargo tarpaulin --skip-clean
cargo tarpaulin --out Html --output-dir coverage
# Python coverage with pytest-cov
uv run pytest tests/ --cov=py_udp --cov-report=html
uv run pytest tests/ --cov=py_udp --cov-report=term-missing
API Reference
UdpServer
Constructor
UdpServer(host: str = "0.0.0.0", port: int = 0)
Methods
bind(host: Optional[str] = None, port: Optional[int] = None) -> None- Bind the server to a specific address and port
set_message_handler(handler: Union[MessageHandler, Callable]) -> None- Set the message handler for processing incoming messages
start() -> None- Start the server and begin listening for messages
stop() -> None- Stop the server and clean up resources
is_running() -> bool- Check if the server is currently running
send_to(data: bytes, address: str) -> int- Send data to a specific address (host:port format)
Properties
address: Tuple[str, int]- Server address
UdpClient
Constructor
UdpClient(host: str = "0.0.0.0", port: int = 0)
Methods
bind(host: Optional[str] = None, port: Optional[int] = None) -> None- Bind the client to a specific address and port
send_to(data: bytes, host: str, port: int) -> int- Send data to a specific host and port
recv_from() -> Tuple[bytes, str]- Receive data from any address
Properties
address: Tuple[str, int]- Client address
MessageHandler
Base class for handling incoming UDP messages:
class MyHandler(MessageHandler):
def __call__(self, data: bytes, source_address: str) -> None:
# Handle the message
# data: received data as bytes
# source_address: source address as string (host:port)
pass
Performance
The Rust backend provides significant performance improvements over pure Python implementations:
- Lower Latency: Direct system calls without Python GIL overhead
- Higher Throughput: Efficient async I/O with Tokio
- Memory Efficiency: Zero-copy operations where possible
- Concurrent Processing: True parallelism for multiple connections
- Channel-based Communication: Non-blocking message passing between Rust and Python
Troubleshooting
Common Issues
- Import Error: Make sure the Rust extension is built with
uv run maturin develop - Permission Denied: Check if the port is already in use or requires elevated privileges
- Build Errors: Ensure you have the latest Rust toolchain installed
- Runtime Errors: Make sure the server is started before calling
send_to()
Debug Mode
Enable debug logging for Rust:
export RUST_LOG=debug
cargo test
Testing the Installation
Run a simple test to verify everything works:
uv run python -c "from py_udp import UdpServer, UdpClient; print('✅ Installation successful!')"
License
MIT License - see LICENSE file for details.
Contributing
- Fork the repository
- Create a feature branch
- Make your changes
- Add tests for both Rust and Python
- Run tests and linting
- Submit a pull request
Development Workflow
-
Rust Changes: Work in
src/directory- Add unit tests in
tests/test_udp.rs - Add integration tests in
tests/integration_udp.rs - Run
cargo testto verify changes - Check coverage with
cargo tarpaulin
- Add unit tests in
-
Python Changes: Work in
py_udp/directory- Add tests in
tests/test_udp.py - Run
uv run pytest tests/to verify changes - Check coverage with
uv run pytest tests/ --cov=py_udp
- Add tests in
-
Integration: Test both components work together
- Run both Rust and Python test suites
- Test examples in
examples/directory - Verify end-to-end functionality
-
Documentation: Update README and docstrings
-
Examples: Update examples in
examples/directory
Code Style
- Rust: Follow Rust formatting guidelines with
cargo fmt - Python: Use Black for formatting and isort for imports
- Documentation: Keep docstrings up to date
- Tests: Maintain good test coverage for both Rust and Python
Release files for py-udp-rust 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| py_udp_rust-0.2.0-cp312-cp312-win_amd64.whl | CPython 3.12 | CPython 3.12 | Windows x86-64 | Details |
Release files / py_udp_rust-0.2.0-cp312-cp312-win_amd64.whl
| Download URL | py_udp_rust-0.2.0-cp312-cp312-win_amd64.whl |
|---|---|
| Size | 346.5 kB |
| Tags | CPython 3.12 Windows x86-64 |
|
SHA-256 checksum How to use checksums |
d73d5a9d0002bd213e4fc0cb4e9a0c1c250afcf140def3e70eac6befb0bfe276
|
|
BLAKE2b-256 checksum How to use checksums |
5d088cdd38b6b9e7016fc0fbf8f1ce725db4229cea2a8b5457a277a99722e73d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.1.0 CPython/3.12.0
|