Skip to content

Python SDK: async methods return () instead of declared None #4356

Open

Description

@FenjuFu

Bug description

Several Python SDK methods are declared as collections.abc.Awaitable[None], but their successful futures return Rust unit (()). With the pinned PyO3 0.29.2 and pyo3-async-runtimes 0.29.0, that path resolves to an empty Python tuple rather than None.

This affects callers that check result is None and makes the runtime contract inconsistent with the generated type declarations.

Affected area / component

Python SDK

Versions

Audited checkout: e0c2423a, based on upstream 3a00b5ba1ebcca9ccfaab532b406c9591a862dcb. PyO3 0.29.2 and pyo3-async-runtimes 0.29.0.

Evidence and validation limits

Source inspection at e0c2423a2961ed6728c0cc92bf377c8a06dc251e identified 24 IggyClient methods and 3 IggyConsumer methods. This audit did not run runtime tests for those 27 methods. The standalone ping reproduction below is prepared but has not yet been run: the available Windows environment lacks the build tools and a usable local Linux/server environment.

The same conversion was observed in the initial delete_segments implementation: two real-server tests failed with assert () is None in this CI run. That new method was separately corrected for #4328. Its successful rerun does not establish runtime coverage for the other methods listed here.

Minimal reproduction to run

Build/install the Python SDK from the audited checkout and start an Iggy server from the same checkout, then run:

import asyncio
from apache_iggy import IggyClient

async def main():
    client = IggyClient("127.0.0.1:8090")
    await client.connect()
    try:
        result = await client.ping()
        print(repr(result), type(result).__name__)
        assert result is None
    finally:
        await client.shutdown()

asyncio.run(main())

Expected: None. Source-predicted result before a fix: () (tuple). The reproduction only opens a connection and pings the server; it does not create or delete resources.

Affected methods from source inspection

  • IggyClient: ping, login_user, update_user, delete_user, update_permissions, change_password, logout_user, connect, disconnect, shutdown, create_stream, update_stream, delete_stream, purge_stream, create_topic, update_topic, delete_topic, purge_topic, create_partitions, delete_partitions, create_consumer_group, delete_consumer_group, join_consumer_group, leave_consumer_group
  • IggyConsumer: store_offset, delete_offset, consume_messages

IggyClient.delete_segments and IggyProducer.shutdown explicitly return Python None and are excluded.

Conversion path

Source references: client bindings, consumer bindings, and type declarations.

  • The wrappers declare Awaitable[None] using a stub override, which does not change runtime conversion.
  • pyo3-async-runtimes 0.29.0 converts the future's successful value with val.into_py_any(py) (src/generic.rs:655).
  • PyO3 0.29.2 implements IntoPyObject for () using PyTuple::empty(py) (src/conversion.rs:561-572).

Related work and scope

#4166 proposes returning resource details from create_stream, create_topic, and create_consumer_group. #4167 proposes returning identity details from login_user. Those four methods should be coordinated with that work rather than assigned a new final return contract here.

For methods intended to retain a None contract, an explicit Python None conversion and a return-value regression test would align runtime behavior with the type declaration. The approach should be agreed before a broad fix. This report is separate from #4328.

Activity

  1. added
    bugSomething isn't working
    pythonPull requests that update Python code
    on Sep 30, 2026
  2. slbotbm commented on Sep 30, 2026

    @slbotbm
    Contributor

    @FenjuFu are you working on this?

  3. hubcio commented on Sep 30, 2026

    @hubcio
    Contributor

    i checked in code and you're right. pyo3 turns () into an empty tuple, and no test checks these return values.

    let's return None like the stubs say. fix 26 of them, including the three from #4166 (it can change them later). login_user is already covered by #4276. use one helper for all of them (and IggyProducer.shutdown) and add one test that calls each method and checks the result is None.

    you're on #4328 with no PR yet, so open that first (one open pr at a time). after it's merged, grab this one if you want. test and fix in one PR is fine.

    do you want to work on this @FenjuFu ?

  4. FenjuFu commented on Oct 8, 2026

    @FenjuFu
    ContributorAuthor

    Yes, I'd like to take this, following hubcio's plan (one helper for all 26 methods plus IggyProducer.shutdown, and one test that checks each returns None). Since I can only have one open PR as a new contributor, I'll start right after #4357 is merged. If someone wants it sooner, feel free to take it.

  5. hubcio commented on Oct 8, 2026

    @hubcio
    Contributor

    fyi purge_stream and purge_topic are going away (we're removing purge, removal pr goes up today), so whichever pr lands second just drops them. no need to change your plan.

  6. FenjuFu commented on Oct 14, 2026

    @FenjuFu
    ContributorAuthor

    #4357 is merged, so I'm free to pick this up. Could you assign it to me? I'll follow the plan above: one helper for the None returns, and one test that checks each method. purge_stream/purge_topic are already gone from master, so they're not part of it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingpythonPull requests that update Python code

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions