Zero is a simple Python framework (RPC like) to build fast and high performance microservices or distributed servers
Features:
- Zero provides faster communication (see benchmarks) between the microservices using zeromq or raw TCP under the hood.
- Zero uses messages for communication and traditional client-server or request-reply pattern is supported.
- Support for both async and sync.
- The base server (ZeroServer) utilizes all cpu cores.
- Built-in support for Pydantic.
- Code generation! See example 👇
Philosophy behind Zero:
- Zero learning curve: The learning curve is tends to zero. Just add functions and spin up a server, literally that's it! The framework hides the complexity of messaging pattern that enables faster communication.
- ZeroMQ: An awesome messaging library enables the power of Zero.
The documentation can be found here.
Ensure Python 3.9+
pip install zeroapi
pip install "zeroapi[uvloop]" # for better async performance on linux and mac-os
pip install "zeroapi[pydantic]" # for pydantic support
pip install "zeroapi[tornado]" # for windows async support
pip install "zeroapi[all]" # for all extras
Create a
server.pyfromzeroimportZeroServerapp=ZeroServer(port=5559) @app.register_rpcdefecho(msg: str) ->str: returnmsg@app.register_rpcasyncdefhello_world() ->str: return"hello world"if__name__=="__main__": app.run()
The RPC functions only support one argument (
msg) for now.Also note that server RPC functions are type hinted. Type hint is must in Zero server. Supported types can be found here.
Run the server
python -m server
Call the rpc methods
fromzeroimportZeroClientzero_client=ZeroClient("localhost", 5559) defecho(): resp=zero_client.call("echo", "Hi there!") print(resp) defhello(): resp=zero_client.call("hello_world", None) print(resp) if__name__=="__main__": echo() hello()
Or using async client -
importasynciofromzeroimportAsyncZeroClientzero_client=AsyncZeroClient("localhost", 5559) asyncdefecho(): resp=awaitzero_client.call("echo", "Hi there!") print(resp) asyncdefhello(): resp=awaitzero_client.call("hello_world", None) print(resp) if__name__=="__main__": loop=asyncio.get_event_loop() loop.run_until_complete(echo()) loop.run_until_complete(hello())
By default Zero uses ZeroMQ for communication. But if you want to use raw TCP, you can use the protocol parameter.
fromzeroimportZeroServerfromzero.protocols.tcpimportTCPServerapp=ZeroServer(port=5559, protocol=TCPServer) # <-- Note the protocol parameter@app.register_rpcdefecho(msg: str) ->str: returnmsg@app.register_rpcasyncdefhello_world() ->str: return"hello world"if__name__=="__main__": app.run()
In that case the client should also use TCP protocol.
importasynciofromzeroimportAsyncZeroClientfromzeroimportZeroClientfromzero.protocols.tcpimportAsyncTCPClientzero_client=ZeroClient("localhost", 5559, protocol=AsyncTCPClient) # <-- Note the protocol parameterasyncdefecho(): resp=awaitzero_client.call("echo", "Hi there!") print(resp) asyncdefhello(): resp=awaitzero_client.call("hello_world", None) print(resp) if__name__=="__main__": loop=asyncio.get_event_loop() loop.run_until_complete(echo()) loop.run_until_complete(hello())
TCP has better performance and throughput than ZeroMQ. We might make it the default protocol in future releases.
Msgspec is the default serializer. So msgspec.Struct (for high performance) or dataclass or any supported types can be used easily to pass complex arguments, i.e.
fromdataclassesimportdataclassfrommsgspecimportStructfromzeroimportZeroServerapp=ZeroServer()
classPerson(Struct):
name: strage: intdob: datetime@dataclassclassOrder:
id: intamount: floatcreated_at: datetime@app.register_rpcdefsave_person(person: Person) ->bool:
# save person to db
...
@app.register_rpcdefsave_order(order: Order) ->bool:
# save order to db
...Pydantic models are also supported out of the box. Just use pydantic.BaseModel as the argument or return type and install zero with pydantic extra.
pip install zeroapi[pydantic]
If you want to use a custom serializer, you can create your own serializer by implementing the Encoder interface.
classMyCustomEncoder(Encoder):
defencode(self, obj: Any) ->bytes:
# implement your custom serialization logic here
...
defdecode(self, data: bytes, type_hint: Type[Any]) ->Any:
# implement your custom deserialization logic here
...Then pass the serializer to both* server and client.
fromzeroimportZeroServer, ZeroClientfrommy_custom_encoderimportMyCustomEncoderapp=ZeroServer(port=5559, encoder=MyCustomEncoder)
zero_client=ZeroClient("localhost", 5559, encoder=MyCustomEncoder)The return type of the RPC function can be any of the supported types. If return_type is set in the client call method, then the return type will be converted to that type.
@dataclassclassOrder:
id: intamount: floatcreated_at: datetimedefget_order(id: str) ->Order:
returnzero_client.call("get_order", id, return_type=Order)Easy to use code generation tool is also provided with schema support!
After running the server, like above, you can generate client code using the
zero.generate_clientmodule.This makes it easy to get the latest schemas on live servers and not to maintain other file sharing approach to manage schemas.
Using
zero.generate_clientgenerate client code for even remote servers using the--host,--port, and--protocoloptions.python -m zero.generate_client --host localhost --port 5559 --protocol zmq --overwrite-dir ./my_client
It will generate client like this -
fromdataclassesimportdataclassfrommsgspecimportStructfromdatetimeimportdatetimefromzeroimportZeroClientzero_client=ZeroClient("localhost", 5559) classPerson(Struct): name: strage: intdob: datetime@dataclassclassOrder: id: intamount: floatcreated_at: datetimeclassRpcClient: def__init__(self, zero_client: ZeroClient): self._zero_client=zero_clientdefsave_person(self, person: Person) ->bool: returnself._zero_client.call("save_person", person) defsave_order(self, order: Order) ->bool: returnself._zero_client.call("save_order", order)
Check the schemas are copied!
Use the client -
frommy_clientimportRpcClient, zero_clientclient=RpcClient(zero_client) if__name__=="__main__": client.save_person(Person(name="John", age=25, dob=datetime.now())) client.save_order(Order(id=1, amount=100.0, created_at=datetime.now()))
To generate async client code, use the
--asyncflag.python -m zero.generate_client --host localhost --port 5559 --protocol zmq --overwrite-dir ./my_async_client --async
*tcp protocol will always generate async client.
ZeroServershould always be run underif __name__ == "__main__":, as it uses multiprocessing.ZeroServercreates the workers in different processes, so anything global in your code will be instantiated N times where N is the number of workers. So if you want to initiate them once, put them underif __name__ == "__main__":. But recommended to not use global vars. And Databases, Redis, other clients, creating them N times in different processes is fine and preferred.
Zero is all about inter service communication. In most real life scenarios, we need to call another microservice.
So we will be testing a gateway calling another server for some data. Check the benchmark/dockerize folder for details.
There are two endpoints in every tests,
/hello: Just call for a hello world response 😅/order: Save a Order object in redis
Compare the results! 👇
13th Gen Intel® Core™ i9-13900HK @ 5.40GHz, 14 cores, 20 threads, 32GB RAM (Docker in Ubuntu 22.04.2 LTS)
(Sorted alphabetically)
| Framework | "hello world" (req/s) | 99% latency (ms) | redis save (req/s) | 99% latency (ms) |
|---|---|---|---|---|
| aiohttp | 33167.69 | 11.89 | 17959.46 | 12.76 |
| aiozmq | 25174.24 | 6.13 | 8850.15 | 10.19 |
| blacksheep | 38025.53 | 8.41 | 16324.19 | 13.54 |
| fastApi | 19682.99 | 9.09 | 12775.97 | 16.28 |
| sanic | 58811.27 | 4.43 | 23622.69 | 9.22 |
| zero(sync) | 27570.85 | 6.65 | 10269.1 | 23.71 |
| zero(async) | 41091.96 | 4.41 | 23996.18 | 8.64 |
| zero(tcp) | 100752.12 | 2.33 | 35812.88 | 13.48 |
Contributors are welcomed 🙏
Please leave a star ⭐ if you like Zero!

