Skip to content

Add type hints in Python#1399

Draft
DomFijan wants to merge 6 commits intomainfrom
feat/add_type_hints
Draft

Add type hints in Python#1399
DomFijan wants to merge 6 commits intomainfrom
feat/add_type_hints

Conversation

@DomFijan
Copy link
Contributor

@DomFijan DomFijan commented Mar 5, 2026

This PR adds missing type hinting for Python function returns.

Description

Many Python functions have missing type hints.

Motivation and Context

Resolves: #1311

How Has This Been Tested?

Locally run mypy succesfully.

Checklist:

@DomFijan DomFijan requested a review from janbridley March 5, 2026 21:36

@L.setter
def L(self, value):
def L(self, value: ScalarLike | Sequence[ScalarLike]) -> None:
Copy link
Contributor

Choose a reason for hiding this comment

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

In cases like this where the valid sequences have a fixed length, I think it might be better to use tuple[ScalarLike, ScalarLike, Scalarlike]. A little more verbose, but gives you meaningful feedback if you pass an incorrectly typed input

def make_absolute(self, fractional_coordinates, out=None):
def make_absolute(
self, fractional_coordinates: ArrayLike, out: ArrayLike | None = None
) -> npt.NDArray[np.floating]:
Copy link
Contributor

Choose a reason for hiding this comment

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

We defined FloatArray, could we use that as the return?

return np.squeeze(out) if flatten else out

def get_images(self, vecs):
def get_images(self, vecs: ArrayLike) -> npt.NDArray[np.integer]:
Copy link
Contributor

Choose a reason for hiding this comment

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

And intarray -- I think we can swap these all out with ruplacer

Comment on lines -38 to 46
def __init__(self, box, basis_positions=None):
def __init__(
self, box: freud.box.Box | ArrayLike, basis_positions: ArrayLike | None = None
) -> None:
if basis_positions is None:
basis_positions = [[0, 0, 0]]
Copy link
Contributor

Choose a reason for hiding this comment

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

Because we automatically convert basis_positions if it is None, could we instead set the default to basis_positions=((0,0,0),)?

def to_image(self, cmap="afmhot", vmin=None, vmax=None):
def to_image(
self, cmap: str = "afmhot", vmin: float | None = None, vmax: float | None = None
) -> np.ndarray:
Copy link
Contributor

@janbridley janbridley Mar 6, 2026

Choose a reason for hiding this comment

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

Suggested change
) -> np.ndarray:
) -> npt.NDArray[np.uint8]:

Full shape annotation is not supported but it's nice to mark this as uint8 imo

Comment on lines +48 to +200
NeighborQueryDict = dict[str, object]


class _CPPNPoint(Protocol):
def getQueryPointIdx(self) -> int: ...
def getPointIdx(self) -> int: ...
def getDistance(self) -> float: ...


class _CPPNeighborList(Protocol):
def copy(self, other: object) -> None: ...
def getNeighbors(self) -> _CPPIntArray: ...
def getWeights(self) -> _CPPFloatArray: ...
def getDistances(self) -> _CPPFloatArray: ...
def getVectors(self) -> _CPPFloatArray: ...
def getSegments(self) -> _CPPIntArray: ...
def getCounts(self) -> _CPPIntArray: ...
def getNumBonds(self) -> int: ...
def getNumQueryPoints(self) -> int: ...
def getNumPoints(self) -> int: ...
def find_first_index(self, i: int) -> int: ...
def filter(self, filt: ArrayLike) -> None: ...
def filter_r(self, r_max: ScalarLike, r_min: ScalarLike) -> None: ...
def sort(self, by_distance: bool) -> None: ...


class _CPPNeighborIterator(Protocol):
def next(self) -> _CPPNPoint: ...
def toNeighborList(self, sort_by_distance: bool) -> _CPPNeighborList: ...


class _CPPNeighborQuery(Protocol):
def query(self, points: ArrayLike, query_args: object) -> _CPPNeighborIterator: ...
def getBox(self) -> freud.box._CPPBox: ...


class _CPPLinkCell(_CPPNeighborQuery, Protocol):
def getCellWidth(self) -> float: ...


class _CPPFloatArray(Protocol):
def toNumpyArray(self) -> npt.NDArray[np.floating]: ...


class _CPPIntArray(Protocol):
def toNumpyArray(self) -> npt.NDArray[np.integer]: ...


class _CPPSpatialHistogram(Protocol):
mode: int

def getBox(self) -> freud.box._CPPBox: ...
def getBinCounts(self) -> _CPPFloatArray: ...
def getBinCenters(self) -> Sequence[npt.NDArray[np.floating]]: ...
def getBinEdges(self) -> Sequence[npt.NDArray[np.floating]]: ...
def getBounds(self) -> list[tuple[float, float]] | tuple[float, float]: ...
def getAxisSizes(self) -> list[int] | int: ...
def reset(self) -> None: ...
def accumulate(self, *args: object) -> None: ...
def getCorrelation(self) -> _CPPFloatArray: ...
def getNBins(self) -> int: ...
def accumulateRDF(self, *args: object) -> None: ...
def getRDF(self) -> _CPPFloatArray: ...
def getNr(self) -> _CPPFloatArray: ...
def getBondOrder(self) -> _CPPFloatArray: ...
def getMode(self) -> _CPPModeEnum: ...
def getPCF(self) -> _CPPFloatArray: ...


class _CPPModeEnum(Protocol):
name: str


class _CPPVoronoi(Protocol):
def compute(self, query: _CPPNeighborQuery) -> None: ...
def getPolytopes(self) -> Sequence[Sequence[Sequence[float]]]: ...
def getVolumes(self) -> _CPPFloatArray: ...
def getNeighborList(self) -> _CPPNeighborList: ...


class _CPPFilter(Protocol):
def compute(
self,
query: _CPPNeighborQuery,
query_points: FloatArray,
nlist: _CPPNeighborList,
qargs: object,
) -> None: ...
def getFilteredNlist(self) -> _CPPNeighborList: ...
def getUnfilteredNlist(self) -> _CPPNeighborList: ...


class _MDATimestep(Protocol):
triclinic_dimensions: ArrayLike
positions: ArrayLike


class _ConfigurationLike(Protocol):
box: ArrayLike
dimensions: int


class _ParticlesWithPosition(Protocol):
position: ArrayLike


class _SnapshotLike(Protocol):
configuration: _ConfigurationLike
particles: _ParticlesWithPosition


class _GarnettFrame(Protocol):
box: object
positions: ArrayLike


class _GarnettFrameNew(_GarnettFrame, Protocol):
position: ArrayLike


class _Hoomd2Box(Protocol):
dimensions: int
Lx: float
Ly: float
xy: float


class _Hoomd2Particles(Protocol):
position: ArrayLike


class _Hoomd2Snapshot(Protocol):
box: _Hoomd2Box
particles: _Hoomd2Particles


class _OvitoCell(Protocol):
matrix: FloatArray
is2D: bool # noqa: N815 - external API naming


class _OvitoParticles(Protocol):
positions: ArrayLike


class _OvitoCollection(Protocol):
cell: _OvitoCell
particles: _OvitoParticles


class _SystemWithBoxPoints(Protocol):
box: object
points: ArrayLike
Copy link
Contributor

Choose a reason for hiding this comment

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

We could consider moving this to a freud/locality_typing.py or similar?

import inspect
from collections.abc import Iterator, Mapping, Sequence
from importlib.util import find_spec
from typing import TYPE_CHECKING, Literal, Protocol, cast
Copy link
Contributor

@janbridley janbridley Mar 6, 2026

Choose a reason for hiding this comment

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

Even in Python we end up with reinterpret cast, it truly is inevitable

@janbridley
Copy link
Contributor

janbridley commented Mar 6, 2026

I'm generally happy with these changes, just a few consistency questions and a nitpick here or there. This will be quite useful to have, it's remarkable to see how far python typing has come.

As a quick heads-up: while the typing is correct at a hight level there are many cases where we don't formally satisfy all the requirements. I don't think this really matters but it does cause a lot of typing errors in LSPs:

image

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.

Add type hinting in python

2 participants