Skip to content

Add type annotations to fdb Python bindings (PEP 561)#13241

Open
sah-rohan wants to merge 21 commits into
apple:mainfrom
sah-rohan:python-typing
Open

Add type annotations to fdb Python bindings (PEP 561)#13241
sah-rohan wants to merge 21 commits into
apple:mainfrom
sah-rohan:python-typing

Conversation

@sah-rohan

@sah-rohan sah-rohan commented May 19, 2026

Copy link
Copy Markdown

Problem

Addresses issue #11331. The fdb Python bindings ship without type annotations and without a py.typed marker. Downstream users get no help from mypy, pyright, or IDEs when calling the public API, and even if annotations existed they would be ignored by PEP 561-compliant type checkers (which require an explicit py.typed file to trust inline annotations from a third-party package).

Solution

  • Add an empty fdb/py.typed marker file and wire it into the sdist/wheel via [tool.setuptools.package-data] in pyproject.toml (PEP 561).
  • Annotate the public surface of fdb/impl.py, fdb/tuple.py, fdb/subspace_impl.py, and fdb/locality.py.
  • Introduce a TupleElement type alias in fdb/tuple.py covering the values the tuple layer can pack (None | bytes | str | int | float | bool | uuid.UUID | SingleFloat | Versionstamp | nested tuple/list), and use it in pack, pack_with_versionstamp, unpack, range, has_incomplete_versionstamp, compare, and across Subspace.
  • Use from __future__ import annotations so all annotations are strings at runtime — zero runtime cost, no import-order issues.

Where Any is intentionally retained

A few annotations remain Any on purpose:

  • Future.wait() / Future.result() return type — Future is a generic base class; concrete subclasses (FutureInt64, FutureString, FutureKeyValueArray, …) already return narrower types.
  • keyToBytes(k) / valueToBytes(v) parameters — duck-typed via the as_foundationdb_key / as_foundationdb_value protocol. Tightening these would require introducing a typing.Protocol; happy to do that in a follow-up.
  • tpointer / fpointer / dpointer constructor params on TransactionRead, Transaction, Future, Database — the codebase passes a mix of int, Optional[int] (from c_void_p().value), and raw ctypes.c_void_p to these. A single accurate type would require touching call sites, which expands scope.
  • *args: Any, **kwargs: Any in Transaction.get_range_startswith — pure forwarding to get_range.

Testing

Ran mypy against both main and this branch using:

@xis19 xis19 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the implementation!

Comment thread bindings/python/fdb/impl.py Outdated
Comment thread bindings/python/fdb/impl.py Outdated
)

def set(self, key, value):
def set(self, key: Any, value: Any) -> None:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would feel bringing a Protocol might be helpful, e.g.

class IFoundationDBKey:
   def as_foundationdb_key(self) -> bytes:
      pass

and key: Union[IFoundationDBKey, bytes]

Not sure if IFoundationDBKey is good but not a native English speaker, so do not know if there is any better choices.

@sah-rohan sah-rohan May 23, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've introduced IFoundationDBKey and IFoundationDBValue as @runtime_checkable Protocols so the type reflects the actual duck-typing contract the code uses. All key/value parameters now use KeyType = Union[bytes, IFoundationDBKey] and ValueType = Union[bytes, IFoundationDBValue].

Comment thread bindings/python/fdb/impl.py Outdated

def __init__(self, fpointer):
def __init__(self, fpointer: Any) -> None:
# print("Creating future 0x%x" % fpointer)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe help removing those debugging prints? Or use logging.Logger?

@sah-rohan sah-rohan May 23, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the commented-out debug prints in Future.init and del have been removed.

Comment thread bindings/python/fdb/impl.py Outdated
def on_ready(self, callback):
def cb_and_delref(ignore):
def on_ready(self, callback: Callable[[Future], None]) -> None:
def cb_and_delref(ignore: object) -> None:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

object to Any?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, maybe just _ignore: Any?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as a matter of fact I guess this depends on type safety as stated in docs:

Use object to indicate that a value could be any type in a typesafe manner. Use Any to indicate that a value is dynamically typed.

Not sure here the ignore is type safe though.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed _ignore: Any → _ignore: object in both cb_and_delref and wait_for_any's closure

Comment thread bindings/python/fdb/impl.py Outdated
for i, f in enumerate(futures):

def cb(ignore, i=i):
def cb(ignore: object, i: int = i) -> None:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe rename the arg i to something else? should be better than shadowing variable

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

renamed to idx

self.on_ready(lambda f: self.call_soon_threadsafe(fn, f))

def remove_done_callback(self, fn):
def remove_done_callback(self, fn: Callable[[Future], None]) -> None:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@abc.abstractmethod?

@sah-rohan sah-rohan May 23, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it should be and I marked it @abc.abstractmethod to match wait(). It always raises NotImplementedError and none of the concrete subclasses (FutureVoid, FutureInt64, etc.) implement it, so making it abstract is the right signal."

Comment thread bindings/python/fdb/impl.py Outdated

class KeySelector(object):
def __init__(self, key, or_equal, offset):
key: bytes

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am confused about this...

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That was a class-level instance variable annotation (PEP 526) that I had added above init, which the diff view made look like it was inside the method body. It was actually redundant as type checkers already infer self.key: bytes from the init parameter annotation, so I've removed the class-level declarations and kept only the init parameter annotations. Sorry for the confusion in the diff!

Comment thread bindings/python/fdb/tuple.py Outdated
import fdb

# Type alias for values that can be packed into a tuple
TupleElement = Union[

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TupleElement: TypeAlias = Union[ ...

@sah-rohan sah-rohan May 23, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Used TupleElement: TypeAlias = Union[...] directly. Since TypeAlias was added in Python 3.10 and we support 3.8+, I added a sys.version_info guard with a fallback to typing_extensions, and added typing_extensions>=4.0 as a conditional dependency in pyproject.toml

_UNSET_TR_VERSION = 10 * int2byte(0xFF)
_STRUCT_FORMAT_STRING = ">" + str(_TR_VERSION_LEN) + "sH"

tr_version: Optional[bytes]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure we need classvar?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The class-level constants (LENGTH, _TR_VERSION_LEN, _MAX_USER_VERSION, etc.) are now annotated with ClassVar[int]/ClassVar[bytes]/ClassVar[str] as appropriate. The tr_version and user_version fields are instance variables (set in init) so they do not use ClassVar, They're declared as plain instance variable annotations in the class body.

@xis19

xis19 commented Jun 2, 2026

Copy link
Copy Markdown
Collaborator

lgtm

@sah-rohan

sah-rohan commented Jun 2, 2026

Copy link
Copy Markdown
Author

Hi @xis19 — thanks for the review! Would you be able to submit a formal approval?

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants