Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
126 commits
Select commit Hold shift + click to select a range
0f5905d
WIP
FBruzzesi Jan 3, 2026
bd7d0d9
chore: Appease `ruff`
dangotbanned Jan 4, 2026
dcd6d79
chore: Appease `mypy`
dangotbanned Jan 4, 2026
560d577
test: xfail a todo
dangotbanned Jan 4, 2026
b40a6a9
refactor: Split out supertyping from `dtypes`
dangotbanned Jan 4, 2026
dade8f2
chore: Define integer bits in the class def
dangotbanned Jan 4, 2026
5892f7a
start adding typing
dangotbanned Jan 4, 2026
36ce6ea
fix: wow
dangotbanned Jan 4, 2026
46c21a2
refactor: Use `IntegerType._bits`
dangotbanned Jan 4, 2026
ea6b143
skip using `DTypes` for `IntegerType`s
dangotbanned Jan 4, 2026
3743462
a bit more typing-friendly
dangotbanned Jan 4, 2026
37ba753
`None` can't return here
dangotbanned Jan 4, 2026
c2d9ddf
refactor: No versioning needed for `Float64`
dangotbanned Jan 4, 2026
48c952d
test: Add ids for tests
dangotbanned Jan 4, 2026
34c8088
change the problem to be which function
dangotbanned Jan 4, 2026
3f4706d
Generate a (cached) `IntegerType` search space instead
dangotbanned Jan 4, 2026
1a0f193
docs: Add links for `Enum`
dangotbanned Jan 5, 2026
9a19344
cheaper float compare
dangotbanned Jan 5, 2026
1e64d47
cheaper int, float compare
dangotbanned Jan 5, 2026
2fd8800
add `DType.__eq__` todo
dangotbanned Jan 5, 2026
7425ecc
avoid repeating binary checks
dangotbanned Jan 5, 2026
69949f0
perf: Use `min(..., key=...)` and move cache for `_min_time_unit`
dangotbanned Jan 5, 2026
9b58520
why not do the same for `FloatType`?
dangotbanned Jan 5, 2026
433e439
test(DRAFT): Try to get more cov
dangotbanned Jan 5, 2026
63a1764
test: Almost full cov
dangotbanned Jan 5, 2026
f5b65ef
test: And yet more coverage
dangotbanned Jan 5, 2026
b5521e4
docs: Add todos for temporal -> numeric
dangotbanned Jan 5, 2026
0074caa
perf: Don't lookup the type you have already!
dangotbanned Jan 5, 2026
9488191
refactor: Generalize `_max_float`
dangotbanned Jan 5, 2026
89043d1
Make `(Date, Datetime)` preferable to Numeric
dangotbanned Jan 5, 2026
1ecdee1
more useful `Struct` todo
dangotbanned Jan 5, 2026
1482572
test: Prepare `(Temporal, Numeric)` cases
dangotbanned Jan 5, 2026
b375a16
test: more test id fiddling
dangotbanned Jan 5, 2026
476e71e
Note a funky idea
dangotbanned Jan 5, 2026
09a8000
getting started on `Struct`
dangotbanned Jan 6, 2026
edb5ede
feat: Add `Struct` supertyping
dangotbanned Jan 6, 2026
7e8b5f6
test: Cover more `Struct`
dangotbanned Jan 6, 2026
15f9c9c
test: Add more `Struct` cases
dangotbanned Jan 6, 2026
131e9ed
rename, reuse `IntoDType -> DType` helpers
dangotbanned Jan 6, 2026
4420d37
oops typo
dangotbanned Jan 6, 2026
bdbb1c5
feat(DRAFT): `DType.__call__` experiment
dangotbanned Jan 7, 2026
b291324
refactor: Tighten-up integer supertyping
dangotbanned Jan 7, 2026
d109a86
Merge remote-tracking branch 'upstream/HEAD' into dtypes/supertyping
dangotbanned Jan 7, 2026
f742054
fix: Require `Enum` categories to match
dangotbanned Jan 8, 2026
9c4e650
chore: Write some todo essays
dangotbanned Jan 8, 2026
08525ef
refactor: Make `dtypes` a package
dangotbanned Jan 8, 2026
ac4286d
perf: Make all *visible* imports module-level
dangotbanned Jan 8, 2026
c251707
perf: Use direct instance checks
dangotbanned Jan 8, 2026
a84dd60
perf: Avoid using `DTypes` when it refs an alias export
dangotbanned Jan 8, 2026
d36fd7d
docs: todo -> note
dangotbanned Jan 8, 2026
7b142cd
Absolutely giant refactor!
dangotbanned Jan 8, 2026
2825970
refactor: use `frozen_dtypes` more
dangotbanned Jan 9, 2026
8448858
refactor: Move things back into `_integer_supertyping`
dangotbanned Jan 9, 2026
6a1a461
fix: TypeError: type 'operator.attrgetter' is not subscriptable
dangotbanned Jan 9, 2026
c7bfd9f
test: Prep some `v1` tests
dangotbanned Jan 9, 2026
85b3938
feat: Impl `_get_supertype_v1`
dangotbanned Jan 9, 2026
cc40d52
test: Cover nested `v1` dtypes
dangotbanned Jan 9, 2026
72173fb
test: `@parametrize` over `Version`
dangotbanned Jan 9, 2026
5542ca8
chore(typing): Widen `list` -> `Collection`
dangotbanned Jan 9, 2026
244537d
test: Fix parameterized `Version` `slow` reuse
dangotbanned Jan 10, 2026
2f2cda9
why are you like this mypy
dangotbanned Jan 10, 2026
c8d1cff
Fix (Decimal, FloatX) case
FBruzzesi Jan 10, 2026
55a7de3
clean up Float dtypes, add issue link
FBruzzesi Jan 10, 2026
443d9cc
split up tests
FBruzzesi Jan 10, 2026
d719bcc
test: Fix more test ids
dangotbanned Jan 10, 2026
da29e9a
Merge branch 'main' into dtypes/supertyping
dangotbanned Jan 10, 2026
36f2a4e
test: Move, rename `dtype_ids`
dangotbanned Jan 10, 2026
6d30302
rename _intersect to _has_intersection
FBruzzesi Jan 10, 2026
2c3b145
test mixed version return None
FBruzzesi Jan 10, 2026
1f38f37
test: fix always true
dangotbanned Jan 10, 2026
84468ea
Merge branch 'dtypes/supertyping' of https://github.com/narwhals-dev/…
dangotbanned Jan 10, 2026
af92d34
check symmetrical, add versions, rm comment
FBruzzesi Jan 10, 2026
42c96ab
merge head
FBruzzesi Jan 10, 2026
6269876
test: Add `numeric_dtype` fixture
dangotbanned Jan 10, 2026
0e9d51b
test: Add `naive_temporal_dtype` fixture
dangotbanned Jan 10, 2026
6cc3d91
cov
dangotbanned Jan 10, 2026
d2c96fe
refactor: Rename `narwhals.dtypes.classes` -> `narwhals.dtypes._classes`
dangotbanned Jan 10, 2026
2eaf88d
refactor: `narwhals.stable.v1._dtypes` -> `narwhals.dtypes._classes_v1`
dangotbanned Jan 10, 2026
e71bc47
perf: Avoid inline imports for `v1` dtypes
dangotbanned Jan 10, 2026
fc9134d
docs: Explain `has_nested` heavy-lifting
dangotbanned Jan 10, 2026
fae966b
flip left,right for test_numeric_promotion
FBruzzesi Jan 10, 2026
085fc4d
refactor: Factor-out `version: Version`
dangotbanned Jan 10, 2026
4b8eed2
I can't believe you've done this
dangotbanned Jan 10, 2026
d681c10
chore: Add a bound to `downcast_time_unit`
dangotbanned Jan 10, 2026
9b67280
docs: Convert `_mixed_supertype` comments
dangotbanned Jan 11, 2026
7670022
docs: Convert `_numeric_supertype` comments
dangotbanned Jan 11, 2026
65faf13
docs: Some attempt at explaining `Struct`
dangotbanned Jan 11, 2026
b3121ea
refactor: merge dispatch tables, add docs
dangotbanned Jan 14, 2026
3f98168
test: Remove `slow` marker
dangotbanned Jan 14, 2026
7d7531f
remove comment
dangotbanned Jan 14, 2026
0309bde
Merge remote-tracking branch 'upstream/main' into dtypes/supertyping
dangotbanned Jan 14, 2026
9ab5748
fix(typing): Ensure all dispatch functions return `DType`
dangotbanned Jan 14, 2026
ff1933d
fix(typing): Mostly get scoping working
dangotbanned Jan 14, 2026
a0bad9d
perf: Reuse cached `DType.__eq__`
dangotbanned Jan 14, 2026
ca45998
chore: Renames, remove unused
dangotbanned Jan 14, 2026
933750a
test: More de-duplicating
dangotbanned Jan 14, 2026
48b51eb
revert: woops, how did those vanish?
dangotbanned Jan 14, 2026
b1221f4
chore: Add `promotion-rules.md` script
dangotbanned Jan 15, 2026
91697b6
docs: Manually generate `promotion-rules.md`
dangotbanned Jan 15, 2026
a272b21
Add promotion rules explainations, move to jinja template
FBruzzesi Jan 16, 2026
9382b84
Feedback adjustment in promotion-rules.md, add mkdocs hook, fix script
FBruzzesi Jan 17, 2026
2924433
duh!
FBruzzesi Jan 17, 2026
f72cd11
duh! part 2
FBruzzesi Jan 17, 2026
e0f8ce5
refactor: Use self-documenting constants instead of index in `time_unit`
dangotbanned Jan 19, 2026
fc66091
Merge branch 'main' into dtypes/supertyping
dangotbanned Jan 19, 2026
d84afa2
promotion-rules.md.jinja feedback adjustments
FBruzzesi Jan 19, 2026
39adca4
fix english
FBruzzesi Jan 19, 2026
fd7dbe4
docs: Template the template for templating
dangotbanned Jan 19, 2026
ed1b614
docs: Remove repeat inline supertype docs (#3411)
dangotbanned Jan 22, 2026
d66d07f
refactor: Deduplicate `v1` dtypes imports
dangotbanned Jan 24, 2026
fdd1bc3
merge main and solve conflicts
FBruzzesi Jan 25, 2026
42caf17
WIP: Add support for (Decimal, Decimal)
FBruzzesi Jan 25, 2026
ec0401f
{Decimal, IntegerType} case
FBruzzesi Jan 25, 2026
b5ae62a
simplify a bit
FBruzzesi Jan 25, 2026
8dbd2ef
fix typing issue + function renaming
FBruzzesi Jan 25, 2026
69b07f3
merge main
FBruzzesi Jan 30, 2026
3ad1639
add support for {List, Array} -> List
FBruzzesi Jan 30, 2026
8d9e053
add support for {String, X} -> String, for X not Binary
FBruzzesi Jan 30, 2026
ef2c7db
update docs for {String, X} and {List, Array} cases
FBruzzesi Jan 30, 2026
88683e0
Merge branch 'main' into dtypes/supertyping
FBruzzesi Jan 31, 2026
8ecb636
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jan 31, 2026
eeb822d
Merge remote-tracking branch 'upstream/main' into dtypes/supertyping
dangotbanned Feb 3, 2026
9ed0d17
fiddle with `(Array, List) -> List`
dangotbanned Feb 3, 2026
548e5b8
refactor: Reduce `String` branching
dangotbanned Feb 3, 2026
bb2846f
Merge branch 'main' into dtypes/supertyping
dangotbanned Feb 16, 2026
8757b4f
refactor: Replace ad-hoc dispatch with custom `@singledispatch` (#3410)
dangotbanned Feb 25, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ site/
todo.md
docs/this.md
docs/api-completeness/*.md
docs/concepts/promotion-rules.md
!docs/api-completeness/index.md

# Lock files
Expand Down
7 changes: 5 additions & 2 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ nav:
- concepts/column_names.md
- concepts/boolean.md
- concepts/null_handling.md
- concepts/promotion-rules.md
- Overhead: overhead.md
- Perfect backwards compatibility policy: backcompat.md
- Extensions and Plugins: extending.md
Expand Down Expand Up @@ -81,8 +82,9 @@ theme:
favicon: assets/logo.svg
logo: assets/logo.svg
features:
- content.code.copy
- content.code.annotate
- content.code.copy
- content.footnote.tooltips
- navigation.footer
- navigation.indexes
- navigation.top
Expand Down Expand Up @@ -126,13 +128,14 @@ plugins:

hooks:
- utils/generate_backend_completeness.py
- utils/generate_supertyping.py
- utils/generate_zen_content.py


markdown_extensions:
- admonition
- md_in_html
- pymdownx.details
- footnotes
- pymdownx.tabbed:
alternate_style: true
- pymdownx.superfences:
Expand Down
83 changes: 83 additions & 0 deletions narwhals/_dispatch.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
from __future__ import annotations

from collections.abc import Callable
from functools import partial
from typing import TYPE_CHECKING, Any, Generic, Protocol, TypeVar, overload

if TYPE_CHECKING:

class Deferred(Protocol):
def __call__(self, f: Callable[..., R], /) -> JustDispatch[R]: ...


__all__ = ["just_dispatch"]

R = TypeVar("R")
R_co = TypeVar("R_co", covariant=True)
PassthroughFn = TypeVar("PassthroughFn", bound=Callable[..., Any])
"""Original function is passed-through unchanged."""


class JustDispatch(Generic[R_co]):
"""Single-dispatch wrapper produced by decorating a function with `@just_dispatch`."""

__slots__ = ("_registry", "_upper_bound")

def __init__(self, function: Callable[..., R_co], /, upper_bound: type[Any]) -> None:
self._upper_bound: type[Any] = upper_bound
self._registry: dict[type[Any], Callable[..., R_co]] = {upper_bound: function}

def dispatch(self, tp: type[Any], /) -> Callable[..., R_co]:
"""Get the implementation for a given type."""
if f := self._registry.get(tp):
return f
if issubclass(tp, self._upper_bound):
f = self._registry[tp] = self._registry[self._upper_bound]
return f
msg = f"{self._registry[self._upper_bound].__name__!r} does not support {tp.__name__!r}"
raise TypeError(msg)

def register(
self, tp: type[Any], *tps: type[Any]
) -> Callable[[PassthroughFn], PassthroughFn]:
"""Register types to dispatch via the decorated function."""

def decorate(f: PassthroughFn, /) -> PassthroughFn:
self._registry.update((tp_, f) for tp_ in (tp, *tps))
return f

return decorate

def __call__(self, arg: object, *args: Any, **kwds: Any) -> R_co:
"""Dispatch on the type of the first argument, passing through all arguments."""
return self.dispatch(arg.__class__)(arg, *args, **kwds)


@overload
def just_dispatch(function: Callable[..., R], /) -> JustDispatch[R]: ...
@overload
def just_dispatch(*, upper_bound: type[Any] = object) -> Deferred: ...
def just_dispatch(
function: Callable[..., R] | None = None, /, *, upper_bound: type[Any] = object
) -> JustDispatch[R] | Deferred:
"""Transform a function into a single-dispatch generic function.

An alternative take on [`@functools.singledispatch`]:
- without [MRO] fallback
- allows [*just*] the types registered and optionally an `upper_bound`

Arguments:
function: Function to decorate, where the body serves as the default implementation.
upper_bound: When there is no registered implementation for a specific type, it must
be a subclass of `upper_bound` to use the default implementation.

Tip:
`@just_dispatch` should only be used to decorate **internal functions** as we lose the docstring.

[`@functools.singledispatch`]: https://docs.python.org/3/library/functools.html#functools.singledispatch
[MRO]: https://docs.python.org/3/howto/mro.html#python-2-3-mro
[*just*]: https://github.com/jorenham/optype/blob/e7221ed1d3d02989d5d01873323bac9f88459f26/README.md#just
"""
if function is not None:
return JustDispatch(function, upper_bound)
return partial(JustDispatch[Any], upper_bound=upper_bound)
79 changes: 79 additions & 0 deletions narwhals/dtypes/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
from __future__ import annotations

from narwhals.dtypes._classes import (
Array,
Binary,
Boolean,
Categorical,
Date,
Datetime,
Decimal,
DType,
Duration,
Enum,
Field,
Float32,
Float64,
FloatType,
Int8,
Int16,
Int32,
Int64,
Int128,
IntegerType,
List,
NestedType,
NumericType,
Object,
SignedIntegerType,
String,
Struct,
TemporalType,
Time,
UInt8,
UInt16,
UInt32,
UInt64,
UInt128,
Unknown,
UnsignedIntegerType,
)

__all__ = [
"Array",
"Binary",
"Boolean",
"Categorical",
"DType",
"Date",
"Datetime",
"Decimal",
"Duration",
"Enum",
"Field",
"Float32",
"Float64",
"FloatType",
"Int8",
"Int16",
"Int32",
"Int64",
"Int128",
"IntegerType",
"List",
"NestedType",
"NumericType",
"Object",
"SignedIntegerType",
"String",
"Struct",
"TemporalType",
"Time",
"UInt8",
"UInt16",
"UInt32",
"UInt64",
"UInt128",
"Unknown",
"UnsignedIntegerType",
]
85 changes: 29 additions & 56 deletions narwhals/dtypes.py → narwhals/dtypes/_classes.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,61 +2,24 @@

import enum
from collections import OrderedDict
from collections.abc import Iterable, Mapping
from collections.abc import Mapping
from datetime import timezone
from itertools import starmap
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, ClassVar

from narwhals._utils import (
_DeferredIterable,
isinstance_or_issubclass,
qualified_type_name,
)
from narwhals._utils import _DeferredIterable, isinstance_or_issubclass
from narwhals.exceptions import InvalidOperationError

if TYPE_CHECKING:
from collections.abc import Iterator, Sequence
from typing import Any
from collections.abc import Iterable, Iterator, Sequence
from typing import Any, Literal

import _typeshed
from typing_extensions import Self, TypeIs
from typing_extensions import Self, TypeAlias

from narwhals.typing import IntoDType, TimeUnit


def _validate_dtype(dtype: DType | type[DType]) -> None:
if not isinstance_or_issubclass(dtype, DType):
msg = (
f"Expected Narwhals dtype, got: {type(dtype)}.\n\n"
"Hint: if you were trying to cast to a type, use e.g. nw.Int64 instead of 'int64'."
)
raise TypeError(msg)


def _is_into_dtype(obj: Any) -> TypeIs[IntoDType]:
return isinstance(obj, DType) or (
isinstance(obj, DTypeClass) and not issubclass(obj, NestedType)
)


def _is_nested_type(obj: Any) -> TypeIs[type[NestedType]]:
return isinstance(obj, DTypeClass) and issubclass(obj, NestedType)


def _validate_into_dtype(dtype: Any) -> None:
if not _is_into_dtype(dtype):
if _is_nested_type(dtype):
name = f"nw.{dtype.__name__}"
msg = (
f"{name!r} is not valid in this context.\n\n"
f"Hint: instead of:\n\n"
f" {name}\n\n"
"use:\n\n"
f" {name}(...)"
)
else:
msg = f"Expected Narwhals dtype, got: {qualified_type_name(dtype)!r}."
raise TypeError(msg)
_Bits: TypeAlias = Literal[8, 16, 32, 64, 128]


class DTypeClass(type):
Expand Down Expand Up @@ -176,6 +139,9 @@ def __eq__(self, other: DType | type[DType]) -> bool: # type: ignore[override]
def __hash__(self) -> int:
return hash(self.__class__)

def __call__(self) -> Self:
return self


class NumericType(DType):
"""Base class for numeric data types."""
Expand All @@ -184,12 +150,19 @@ class NumericType(DType):
class IntegerType(NumericType):
"""Base class for integer data types."""

# NOTE: Likely going to need an `Integer` metaclass, to be able to use `Final` or a class property
_bits: ClassVar[_Bits]

def __init_subclass__(cls, *args: Any, bits: _Bits, **kwds: Any) -> None:
super().__init_subclass__(*args, **kwds)
cls._bits = bits


class SignedIntegerType(IntegerType):
class SignedIntegerType(IntegerType, bits=128):
"""Base class for signed integer data types."""


class UnsignedIntegerType(IntegerType):
class UnsignedIntegerType(IntegerType, bits=128):
"""Base class for unsigned integer data types."""


Expand Down Expand Up @@ -261,7 +234,7 @@ def __repr__(self) -> str: # pragma: no cover
return f"{class_name}(precision={self.precision!r}, scale={self.scale!r})"


class Int128(SignedIntegerType):
class Int128(SignedIntegerType, bits=128):
"""128-bit signed integer type.

Examples:
Expand All @@ -281,7 +254,7 @@ class Int128(SignedIntegerType):
"""


class Int64(SignedIntegerType):
class Int64(SignedIntegerType, bits=64):
"""64-bit signed integer type.

Examples:
Expand All @@ -294,7 +267,7 @@ class Int64(SignedIntegerType):
"""


class Int32(SignedIntegerType):
class Int32(SignedIntegerType, bits=32):
"""32-bit signed integer type.

Examples:
Expand All @@ -307,7 +280,7 @@ class Int32(SignedIntegerType):
"""


class Int16(SignedIntegerType):
class Int16(SignedIntegerType, bits=16):
"""16-bit signed integer type.

Examples:
Expand All @@ -320,7 +293,7 @@ class Int16(SignedIntegerType):
"""


class Int8(SignedIntegerType):
class Int8(SignedIntegerType, bits=8):
"""8-bit signed integer type.

Examples:
Expand All @@ -333,7 +306,7 @@ class Int8(SignedIntegerType):
"""


class UInt128(UnsignedIntegerType):
class UInt128(UnsignedIntegerType, bits=128):
"""128-bit unsigned integer type.

Examples:
Expand All @@ -347,7 +320,7 @@ class UInt128(UnsignedIntegerType):
"""


class UInt64(UnsignedIntegerType):
class UInt64(UnsignedIntegerType, bits=64):
"""64-bit unsigned integer type.

Examples:
Expand All @@ -360,7 +333,7 @@ class UInt64(UnsignedIntegerType):
"""


class UInt32(UnsignedIntegerType):
class UInt32(UnsignedIntegerType, bits=32):
"""32-bit unsigned integer type.

Examples:
Expand All @@ -373,7 +346,7 @@ class UInt32(UnsignedIntegerType):
"""


class UInt16(UnsignedIntegerType):
class UInt16(UnsignedIntegerType, bits=16):
"""16-bit unsigned integer type.

Examples:
Expand All @@ -386,7 +359,7 @@ class UInt16(UnsignedIntegerType):
"""


class UInt8(UnsignedIntegerType):
class UInt8(UnsignedIntegerType, bits=8):
"""8-bit unsigned integer type.

Examples:
Expand Down
Loading
Loading