You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Note: This analysis was performed with AI assistance (Claude Code). The AI ran mypy, pyright, and ty against the full codebase and all 200+ examples, categorized every error, identified root causes, and drafted the suggested fixes. I reviewed and validated the results. Using AI for this type of systematic audit across multiple type checkers saved significant time while ensuring nothing was missed.
pydynox fails type checking under strict mode in all three major Python type checkers: mypy (--strict), pyright (basic mode), and ty. Users who enable strict type checking in their projects — which is increasingly common — see a wall of errors when importing and using pydynox.
mypy --strict: 102 errors across 12 files pyright: 22 errors, 7 warnings across 5 files ty: 0 errors (already fixed in #311)
Beyond the internal errors, the public API typing is the main pain point users report. Methods like get(), batch_get(), query(), and scan() return union types that force users to narrow manually, even in the most common usage patterns.
Part 1 — Public API typing issues (user-facing)
These are what users actually hit. No internal errors — these show up in user code.
1A. as_dict parameter doesn't narrow the return type
as_dict=False (default) should return M | None, but the type is always the full union. Affects 12 methods across Model:
# What the user writes (most common case):user=User.sync_get(pk="USER#1")
# Type checker sees: User | dict[str, Any] | None ← user expects User | Noneuser.name# error: "dict[str, Any]" has no attribute "name"# Same with batch_get:users=User.sync_batch_get(keys)
# Type checker sees: list[User] | list[dict[str, Any]] ← user expects list[User]foruinusers:
print(u.name) # error: "dict[str, Any]" has no attribute "name"
Methods affected (sync + async = x2):
Method
Current return type
Expected with as_dict=False
Expected with as_dict=True
get / sync_get
M | dict | None
M | None
dict | None
batch_get / sync_batch_get
list[M] | list[dict]
list[M]
list[dict]
parallel_scan / sync_parallel_scan
tuple[list[M] | list[dict], Metrics]
tuple[list[M], Metrics]
tuple[list[dict], Metrics]
Fix: Add @overload with Literal[True] / Literal[False] for as_dict:
Fix: Make result classes generic over the as_dict flag, or split into two result types, or use @overload on the Model methods that create them:
# Option A: overload at the Model level@overload@classmethoddefsync_query(cls: type[M], ..., as_dict: Literal[True], ...) ->ModelQueryResult[dict[str, Any]]: ...
@overload@classmethoddefsync_query(cls: type[M], ..., as_dict: Literal[False] = ..., ...) ->ModelQueryResult[M]: ...
Then ModelQueryResult.__next__ returns M (or dict based on the generic param).
1C. from_dict returns M but the metaclass makes type checkers lose the type
# User writes:user=User.from_dict({"pk": "USER#1", "name": "John"})
# Some type checkers see: ModelBase ← user expects User
The from_dict classmethod is defined on ModelBase and returns M, but the metaclass pattern can confuse some type checkers about what cls resolves to.
1D. Attribute descriptors return T | None but users expect T for required fields
classUser(Model):
pk=StringAttribute(partition_key=True) # always required, never Nonename=StringAttribute() # always requireduser=User.sync_get(pk="USER#1")
ifuser:
reveal_type(user.pk) # str | None ← user expects str (it's a key, always present)reveal_type(user.name) # str | None ← user expects str (required field)
The __get__ descriptor always returns T | None regardless of whether the attribute is required. This causes downstream noise in every line that accesses an attribute.
Fix: This is the hardest one. Options:
Use @overload on __get__ to narrow based on required/optional (complex)
Accept T | None and document that users should use assert or if for narrowing
Add a py.typed marker and consider whether T alone is safe for required fields
1E. Client-level return_values overloads exist but Model-level doesn't expose them
The client put_item, delete_item, update_item already have proper @overload for return_values:
But Model sync_save, sync_delete, sync_update don't expose return_values at all — they always return None. This is not a typing issue per se, but users who need the old item must drop to the client level.
Part 2 — Internal type checker errors
These are errors inside pydynox source that show up when running type checkers on the library itself. Users don't see them directly but they signal quality issues.
After the _MixinBase typing fix in #311, the # type: ignore[attr-defined] comments in the mixin operation files became obsolete. mypy strict mode flags every one of them as [unused-ignore].
The Rust client (pydynox_core.DynamoDBClient) is a PyO3 extension. mypy sees all its return values as Any. When a Python method has a proper return type annotation but returns a value from the Rust client, mypy flags it.
# _crud.py — mypy sees result["item"] as Anyasyncdefget_item(...) ->dict[str, Any] |None:
result=awaitself._client.get_item(...) # _client is Any → result is Anyreturnresult["item"] # error: Returning Any from function declared to return "dict[str, Any] | None"
Fix: Add explicit type annotations to intermediate variables or use cast():
Category 3: reportOptionalMemberAccess on metrics (14 errors — pyright only)
In query.py, self._last_metrics is typed as OperationMetrics | None but accessed without a None guard in the scan/query result logging code.
File: query.py (lines 314-327, 625-637)
# self._last_metrics can be None_log_operation("query", ..., self._last_metrics.duration_ms, ...) # pyright: "duration_ms" is not a known attribute of "None"
Fix: Add a guard or assert before accessing metrics:
Category 4: has_template not on Attribute (6 errors — pyright only)
pyright doesn't see has_template as a valid attribute on Attribute[Any] because it's only present on template-capable subclasses. The hasattr() guard works at runtime but pyright doesn't narrow the type.
Fix: Already partially fixed in #311 with cast(_TemplateAttr, attr). The remaining pyright errors are on the hasattr check line itself. Use a TypeGuard helper:
integrations/pydantic.py passes functions to add_dynamodb_methods where the TypeVar T gets widened to T | BaseModel after the issubclass check. Also, helper functions in _base.py use T only once in their signature.
Fix: Already partially addressed in #311 with cast(Any, ...). The warnings about single-use TypeVar can be fixed by using Any instead in the internal helper functions (they're not public API).
Breaking Changes
None. All fixes are additive:
@overload additions don't change runtime behavior — only type inference improves
Removing type: ignore comments has zero runtime effect
Adding cast() or type annotations has zero runtime effect
Result class generic parameter changes are type-level only
The only thing users might notice: methods that previously required assert isinstance(result, User) will now just work. Code that relies on the union type (e.g., isinstance checks) will continue to work.
Expected Behavior
mypy --strict, pyright, and ty should all report 0 errors on the pydynox source
User.sync_get(pk="...") should infer User | None, not User | dict | None
for u in User.sync_query(...) should infer u: User, not u: User | dict
User.sync_batch_get(keys) should infer list[User], not list[User] | list[dict]
Actual Behavior
102 mypy strict errors, 22 pyright errors, 7 pyright warnings. Public API return types are always the widest union regardless of parameters.
Environment
pydynox version: 1.1.0
Python version: 3.11+
mypy version: 1.19.1
pyright version: 1.1.408
ty version: 0.0.25
Suggested Fix
Phase 1 — Public API overloads (highest user impact)
Add @overload for as_dict parameter on all 12 affected Model methods (get, sync_get, batch_get, sync_batch_get, parallel_scan, sync_parallel_scan, query, sync_query, scan, sync_scan + result iterators). No runtime changes.
Phase 2 — Bulk cleanup (73 internal errors)
Remove all unused # type: ignore[attr-defined] comments from the 6 mixin files.
Add explicit type annotations to variables that receive values from the Rust client.
Phase 4 — pyright-specific fixes (22 errors)
Add TypeGuard helper for has_template checks
Add None guards before accessing self._last_metrics in query/scan result classes
Fix BaseModel | None in issubclass call
Phase 5 — Minor fixes (11 errors)
Annotate part_placeholders: list[str] in _results.py
Rename reused variables in _helpers.py
Fix type annotations in _model/_base.py metaclass
Add pytest.fixture typing in pytest_plugin.py
All changes are safe, mechanical, and testable. The existing test suite (753 unit + 730 integration + 200 examples) covers all affected code paths.
Phase 6 — Fix all 200+ examples for type correctness (565 pyright errors)
The docs/examples/ directory has 565 pyright errors across 200+ example files. These examples are what users copy — they must be 100% type-clean.
Error breakdown by root cause:
Root Cause
Count
%
Fix
as_dict union: dict has no attribute X
263
47%
Fixed by Phase 1 overloads — examples won't need changes
T|None: accessing attr on possibly None
106
19%
Add None guards / assertions in examples
Async method not awaited
47
8%
Add await or switch to sync_ methods
Other attribute access issues
41
7%
Various — S3, inheritance, etc.
Missing imports (third-party)
33
6%
Add # pyright: reportMissingImports=false or stub
Argument type mismatch
29
5%
Fix example code to match signatures
Assignment / call / operator issues
46
8%
Various fixes
Errors by directory (top 10):
Directory
Errors
Notes
agentic/
138
Third-party imports + union types
query/
51
Union types from query results
models/
50
Attribute access on T|None
indexes/
31
Template/union type issues
optimistic_locking/
30
T|None attribute access
conditions/
24
Union types
testing/
29
Memory backend typing
atomic/
26
T|None attribute access
s3/
20
S3Value typing
smart_save/
19
T|None attribute access
Key insight: 47% of example errors (263) will be automatically fixed by Phase 1 (@overload for as_dict). After that, the remaining ~300 errors are mainly examples not handling None properly — which is both a code fix in the examples AND an opportunity to teach good patterns.
Fix approach:
After Phase 1, re-run pyright on examples — expect ~263 errors to disappear
Fix async examples: add await or switch to sync_ variants
Add proper None handling patterns (assert, if-guard, or or default)
For third-party imports (smolagents, pydantic_ai, strands), add # pyright: reportMissingImports=false at file top
Fix type_checking/crud_types.py — currently broken (uses async methods without await, wrong type annotations)
Phase 7 — Rewrite type-checking documentation
The current docs/guides/type-checking.md has several problems:
Recommends workarounds instead of proper typing: tells users to use isinstance and cast() for union types — these are workarounds for the missing @overload that Phase 1 fixes
Example file broken: crud_types.py (included via --8<--) has 8 pyright errors — async methods without await, wrong type annotations
Missing best practices: no mention of strict mode, no pyright config example, no section on reveal_type() debugging
No mention of overloaded return types: after Phase 1, the doc should explain that as_dict=False (default) gives you User | None and as_dict=True gives you dict | None
Rewrite plan:
Update type checker matrix (ty → ✅ Tested, all three pass)
Remove workaround sections (isinstance/cast for union types) — replace with "it just works"
Add strict mode configuration examples for mypy, pyright, and ty
Rewrite crud_types.py example to show proper typed usage
Add section on reveal_type() for debugging type issues
Add section on attribute T | None and when to use assertions
Add pyright pyrightconfig.json example
Add mypy mypy.ini strict config example
Expected Behavior (updated)
After all 7 phases:
mypy --strict, pyright, and ty should all report 0 errors on the pydynox source
pyright should report 0 errors on all 200+ example files
User.sync_get(pk="...") should infer User | None, not User | dict | None
for u in User.sync_query(...) should infer u: User, not u: User | dict
User.sync_batch_get(keys) should infer list[User], not list[User] | list[dict]
The type-checking guide should demonstrate zero-workaround typing
All examples should serve as copy-paste-ready code with clean types
Hey @Bear-03, I'll try to fix this.
Description
pydynox fails type checking under strict mode in all three major Python type checkers: mypy (
--strict), pyright (basic mode), and ty. Users who enable strict type checking in their projects — which is increasingly common — see a wall of errors when importing and using pydynox.mypy --strict: 102 errors across 12 files
pyright: 22 errors, 7 warnings across 5 files
ty: 0 errors (already fixed in #311)
Beyond the internal errors, the public API typing is the main pain point users report. Methods like
get(),batch_get(),query(), andscan()return union types that force users to narrow manually, even in the most common usage patterns.Part 1 — Public API typing issues (user-facing)
These are what users actually hit. No internal errors — these show up in user code.
1A.
as_dictparameter doesn't narrow the return typeas_dict=False(default) should returnM | None, but the type is always the full union. Affects 12 methods across Model:Methods affected (sync + async = x2):
as_dict=Falseas_dict=Trueget/sync_getM | dict | NoneM | Nonedict | Nonebatch_get/sync_batch_getlist[M] | list[dict]list[M]list[dict]parallel_scan/sync_parallel_scantuple[list[M] | list[dict], Metrics]tuple[list[M], Metrics]tuple[list[dict], Metrics]Fix: Add
@overloadwithLiteral[True]/Literal[False]foras_dict:This is not a breaking change — runtime behavior is identical. Only type inference improves.
1B. Query/scan iterators yield
M | dict[str, Any]instead ofMEven without
as_dict, the iterators always yield the union:Affected classes:
ModelQueryResult[M],AsyncModelQueryResult[M],ModelScanResult[M],AsyncModelScanResult[M]Current:
Fix: Make result classes generic over the
as_dictflag, or split into two result types, or use@overloadon the Model methods that create them:Then
ModelQueryResult.__next__returnsM(ordictbased on the generic param).1C.
from_dictreturnsMbut the metaclass makes type checkers lose the typeThe
from_dictclassmethod is defined onModelBaseand returnsM, but the metaclass pattern can confuse some type checkers about whatclsresolves to.1D. Attribute descriptors return
T | Nonebut users expectTfor required fieldsThe
__get__descriptor always returnsT | Noneregardless of whether the attribute is required. This causes downstream noise in every line that accesses an attribute.Fix: This is the hardest one. Options:
@overloadon__get__to narrow based on required/optional (complex)T | Noneand document that users should useassertoriffor narrowingpy.typedmarker and consider whetherTalone is safe for required fields1E. Client-level
return_valuesoverloads exist but Model-level doesn't expose themThe client
put_item,delete_item,update_itemalready have proper@overloadforreturn_values:But Model
sync_save,sync_delete,sync_updatedon't exposereturn_valuesat all — they always returnNone. This is not a typing issue per se, but users who need the old item must drop to the client level.Part 2 — Internal type checker errors
These are errors inside pydynox source that show up when running type checkers on the library itself. Users don't see them directly but they signal quality issues.
Category 1: Unused
type: ignorecomments (73 errors — mypy only)After the
_MixinBasetyping fix in #311, the# type: ignore[attr-defined]comments in the mixin operation files became obsolete. mypy strict mode flags every one of them as[unused-ignore].Files affected:
_crud.py(38),_batch.py(12),_scan.py(10),_query.py(4),_table.py(8),_partiql.py(4)Fix: Remove all
# type: ignore[attr-defined]comments from these 6 mixin files. The_MixinBasebase class already provides proper type information.Category 2:
no-any-returnfrom Rust client calls (18 errors — mypy only)The Rust client (
pydynox_core.DynamoDBClient) is a PyO3 extension. mypy sees all its return values asAny. When a Python method has a proper return type annotation but returns a value from the Rust client, mypy flags it.Files affected:
_crud.py(16),attributes/base.py(2),_results.py(1),testing/memory.py(1)Fix: Add explicit type annotations to intermediate variables or use
cast():Category 3:
reportOptionalMemberAccesson metrics (14 errors — pyright only)In
query.py,self._last_metricsis typed asOperationMetrics | Nonebut accessed without a None guard in the scan/query result logging code.File:
query.py(lines 314-327, 625-637)Fix: Add a guard or assert before accessing metrics:
Category 4:
has_templatenot onAttribute(6 errors — pyright only)pyright doesn't see
has_templateas a valid attribute onAttribute[Any]because it's only present on template-capable subclasses. Thehasattr()guard works at runtime but pyright doesn't narrow the type.Files:
_indexes.py,_model/_base.py,_model/_query.py,collection.pyFix: Already partially fixed in #311 with
cast(_TemplateAttr, attr). The remaining pyright errors are on thehasattrcheck line itself. Use aTypeGuardhelper:Category 5: Minor type issues (11 errors — mypy)
var-annotated(2)_results.py:77,127list[str]annotation topart_placeholdersno-redef(2)_helpers.py:240,344values→val_map,cond_names→merged_names)assignment— None to type (1)_model/_base.py:149type | Noneassignment— LSI to GSI (1)_model/_base.py:165untyped-decorator(4)pytest_plugin.py@pytest.fixturetype stub or castno-untyped-call(1)testing/memory.py:990Category 6: Pydantic integration variance (2 errors pyright, 7 warnings pyright)
integrations/pydantic.pypasses functions toadd_dynamodb_methodswhere the TypeVarTgets widened toT | BaseModelafter theissubclasscheck. Also, helper functions in_base.pyuseTonly once in their signature.Fix: Already partially addressed in #311 with
cast(Any, ...). The warnings about single-use TypeVar can be fixed by usingAnyinstead in the internal helper functions (they're not public API).Breaking Changes
None. All fixes are additive:
@overloadadditions don't change runtime behavior — only type inference improvestype: ignorecomments has zero runtime effectcast()or type annotations has zero runtime effectThe only thing users might notice: methods that previously required
assert isinstance(result, User)will now just work. Code that relies on the union type (e.g.,isinstancechecks) will continue to work.Expected Behavior
mypy --strict,pyright, andtyshould all report 0 errors on the pydynox sourceUser.sync_get(pk="...")should inferUser | None, notUser | dict | Nonefor u in User.sync_query(...)should inferu: User, notu: User | dictUser.sync_batch_get(keys)should inferlist[User], notlist[User] | list[dict]Actual Behavior
102 mypy strict errors, 22 pyright errors, 7 pyright warnings. Public API return types are always the widest union regardless of parameters.
Environment
Suggested Fix
Phase 1 — Public API overloads (highest user impact)
Add
@overloadforas_dictparameter on all 12 affected Model methods (get,sync_get,batch_get,sync_batch_get,parallel_scan,sync_parallel_scan,query,sync_query,scan,sync_scan+ result iterators). No runtime changes.Phase 2 — Bulk cleanup (73 internal errors)
Remove all unused
# type: ignore[attr-defined]comments from the 6 mixin files.Phase 3 — Rust client return types (18 internal errors)
Add explicit type annotations to variables that receive values from the Rust client.
Phase 4 — pyright-specific fixes (22 errors)
TypeGuardhelper forhas_templatechecksself._last_metricsin query/scan result classesBaseModel | NoneinissubclasscallPhase 5 — Minor fixes (11 errors)
part_placeholders: list[str]in_results.py_helpers.py_model/_base.pymetaclasspytest.fixturetyping inpytest_plugin.pyAll changes are safe, mechanical, and testable. The existing test suite (753 unit + 730 integration + 200 examples) covers all affected code paths.
Phase 6 — Fix all 200+ examples for type correctness (565 pyright errors)
The
docs/examples/directory has 565 pyright errors across 200+ example files. These examples are what users copy — they must be 100% type-clean.Error breakdown by root cause:
as_dictunion:dicthas no attribute XT|None: accessing attr on possibly Noneawaitor switch tosync_methods# pyright: reportMissingImports=falseor stubErrors by directory (top 10):
agentic/query/models/T|Noneindexes/optimistic_locking/T|Noneattribute accessconditions/testing/atomic/T|Noneattribute accesss3/smart_save/T|Noneattribute accessKey insight: 47% of example errors (263) will be automatically fixed by Phase 1 (
@overloadforas_dict). After that, the remaining ~300 errors are mainly examples not handlingNoneproperly — which is both a code fix in the examples AND an opportunity to teach good patterns.Fix approach:
awaitor switch tosync_variantsordefault)# pyright: reportMissingImports=falseat file toptype_checking/crud_types.py— currently broken (uses async methods without await, wrong type annotations)Phase 7 — Rewrite type-checking documentation
The current
docs/guides/type-checking.mdhas several problems:isinstanceandcast()for union types — these are workarounds for the missing@overloadthat Phase 1 fixescrud_types.py(included via--8<--) has 8 pyright errors — async methods without await, wrong type annotationsreveal_type()debuggingas_dict=False(default) gives youUser | Noneandas_dict=Truegives youdict | NoneRewrite plan:
crud_types.pyexample to show proper typed usagereveal_type()for debugging type issuesT | Noneand when to use assertionspyrightconfig.jsonexamplemypy.inistrict config exampleExpected Behavior (updated)
After all 7 phases:
mypy --strict,pyright, andtyshould all report 0 errors on the pydynox sourcepyrightshould report 0 errors on all 200+ example filesUser.sync_get(pk="...")should inferUser | None, notUser | dict | Nonefor u in User.sync_query(...)should inferu: User, notu: User | dictUser.sync_batch_get(keys)should inferlist[User], notlist[User] | list[dict]