Skip to content

Add type annotations for rings and parents#41232

Merged
vbraun merged 11 commits intosagemath:developfrom
tobiasdiez:typing-catrings
Jan 25, 2026
Merged

Add type annotations for rings and parents#41232
vbraun merged 11 commits intosagemath:developfrom
tobiasdiez:typing-catrings

Conversation

@tobiasdiez
Copy link
Copy Markdown
Contributor

@tobiasdiez tobiasdiez commented Nov 25, 2025

Add a bit more typing annotations/stubs for a few fundamental objects (with the goal to make typing of the higher level Python interface easier). They are not meant to be 100% accurate and contain a few to many Anys for my taste, but it should serve as a good start.

📝 Checklist

  • The title is concise and informative.
  • The description explains in detail what this PR is about.
  • I have linked a relevant issue or discussion.
  • I have created tests covering the changes.
  • I have updated the documentation and checked the documentation preview.

⌛ Dependencies

@tobiasdiez tobiasdiez force-pushed the typing-catrings branch 2 times, most recently from c7768dd to fab7698 Compare November 25, 2025 08:11
Copy link
Copy Markdown
Member

@vincentmacri vincentmacri left a comment

Choose a reason for hiding this comment

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

Good overall. Just a few questions to make sure I understand this right.

My biggest comment is more of a question. I believe type checkers try to enforce some kind of Liskov substitution principle stuff, which would mean that annotations like def __hash__(self) -> int: are redundant if that annotation is already present in SageObject. So some of the type annotations would be redundant if I'm correct about that.

Comment thread src/sage/categories/map.pyi Outdated

def __init__(self, parent: Any, codomain: Parent[Any] | None = ...) -> None: ...
def __copy__(self) -> Self: ...
def parent(self) -> Any: ...
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Why isn't this annotated to return Parent?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

If I understand the code correctly, its actually always a Homset. I've updated the code now to reflect this.

Comment thread src/sage/categories/map.pyi Outdated
Comment on lines +9 to +10
DomainElementT_contra = TypeVar("DomainElementT_contra", contravariant=True)
CodomainElementT_co = TypeVar("CodomainElementT_co", covariant=True)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I've seen contravariant and covariant in the Python docs before but haven't really seen them used. For the sake of helping me review this, could you briefly explain what these are doing?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This specifies what concrete types can be substituted, with

  • Covariance means that a type can be substituted with a more specific type
  • Contravariance means that a type can be substituted with a more general type

So if you specify Map[Animal, Animal], then for an implementation to be substitutable:

  • It must accept at least all Animal keys (contravariance)
  • It must return at most Animal values (covariance)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

That being said, since we now use Python 3.12 we can use the new Generic syntax which doesn't need this variance anymore.

def __reduce__(self) -> tuple[Any, tuple[Any, ...]]: ...
def _repr_type(self) -> str: ...
def _repr_defn(self) -> str: ...
def _repr_(self) -> str: ...
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Shouldn't the type annotation for _repr_ (and other methods like __hash__) be inherited from SageObject? I though type checkers enforced that through the Liskov substitution principle (at least some do), which would mean that this annotation is redundant.

If I'm correct about the behaviour of type annotations with inheritance, that would mean that we would get the most value for the amount of work by annotating classes higher up in the inheritance hierarchy (like you've done in this PR).

Copy link
Copy Markdown
Member

@vincentmacri vincentmacri Dec 11, 2025

Choose a reason for hiding this comment

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

Shouldn't the type annotation for _repr_ (and other methods like __hash__) be inherited from SageObject? I though type checkers enforced that through the Liskov substitution principle (at least some do), which would mean that this annotation is redundant.

I was playing around with adding annotations to Integer and Element and at least mypy does indeed give an error if the annotations for Integer conflict with the annotations for Element. I didn't check for pyright but I assume it's the same. So I think it should suffice to only annotate methods like _repr_ in SageObject, you don't need to duplicate the annotation (or I think include it in the .pyi file at all if it's already in SageObject. The type checker will be aware of the existing annotation in the superclass.

You can use @override if you want to be more specific about types in a subclass.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

That's correct, but there is currently no typing info for Element. I've added @override now, which is good practice anyway (typing checkers will complain if the type in the subclass is different or the method doesn't exist in the parent). Thanks!

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

That's correct, but there is currently no typing info for Element.

To avoid potential duplicate efforts, just a heads up that I'm working on adding typing info for Element (vincentmacri#22 if you're curious, but it is not remotely ready yet, I'm still experimenting with type checker behaviour and how to get type checking to interact properly with the coercion system).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

To avoid potential duplicate efforts, just a heads up that I'm working on adding typing info for Element (vincentmacri#22 if you're curious, but it is not remotely ready yet, I'm still experimenting with type checker behaviour and how to get type checking to interact properly with the coercion system).

Typing for elements seems to kind of depend on this PR. Between mypy, pyright, and ty, only mypy is able to properly handle when a correctly-typed file interacts with an incorrectly-typed file.

@fchapoton
Copy link
Copy Markdown
Contributor

note that "ParentWithGens" is something we should get rid of. Efforts in this direction are currentl blocked at the ticket about moving "fraction_field" method #40213

@github-actions
Copy link
Copy Markdown

github-actions bot commented Dec 19, 2025

Documentation preview for this PR (built with commit c2cc5e7; changes) is ready! 🎉
This preview will update shortly after each push to this PR.

@tobiasdiez
Copy link
Copy Markdown
Contributor Author

@vincentmacri Thanks for the review, and sorry that it took me so long to come back to you (here and in other PRs). I hope I've addressed all your remarks now.

Copy link
Copy Markdown
Member

@vincentmacri vincentmacri left a comment

Choose a reason for hiding this comment

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

I noticed a few recurring issues that I want to ask about before I finish reviewing this.

  1. Is there a reason why you use ... for the default value of methods that have an explicit default value in the implementation?
  2. I was under the impression that type stub files should only list Python-accessible functions. For Cython code, this means that cdef functions/methods should not be included in the type stub file, but you seem to have included at least a few cdef functions/methods (there might be more that I didn't notice than just the ones I flagged). Am I misunderstanding type stub files or were these cdef functions/methods included in the stub files by mistake?

Comment thread src/sage/rings/ring.pyi Outdated
Comment on lines +24 to +26
names: NameSpec = ...,
normalize: bool = ...,
category: Category | None = ...,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
names: NameSpec = ...,
normalize: bool = ...,
category: Category | None = ...,
names: NameSpec = None,
normalize: bool = True,
category: Category | None = None,

These are the actual defaults used in the implementation file. Is there a reason not to use the actual defaults here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I thought pyi files should not contain default values, but you and python/typeshed#8988 proved me wrong. Thanks!

Comment thread src/sage/rings/ring.pyi Outdated
Comment on lines +44 to +46
names: NameSpec = ...,
normalize: bool = ...,
category: Category | None = ...,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
names: NameSpec = ...,
normalize: bool = ...,
category: Category | None = ...,
names: NameSpec = None,
normalize: bool = True,
category: Category | None = None,

Same as above.

Comment thread src/sage/rings/ring.pyi Outdated
Comment on lines +52 to +53
name: str | None = ...,
names: NameSpec = ...,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
name: str | None = ...,
names: NameSpec = ...,
name: str | None = None,
names: NameSpec = None,

Comment thread src/sage/structure/parent.pyi Outdated
Comment on lines +16 to +19
def is_Integer(x: Any) -> bool: ...
def is_Parent(x: Any) -> bool: ...
def good_as_coerce_domain(S: Any) -> bool: ...
def good_as_convert_domain(S: Any) -> bool: ...
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
def is_Integer(x: Any) -> bool: ...
def is_Parent(x: Any) -> bool: ...
def good_as_coerce_domain(S: Any) -> bool: ...
def good_as_convert_domain(S: Any) -> bool: ...
def is_Parent(x: Any) -> bool: ...

I thought only Python-accessible names go in the type stub files? Aren't the functions is_Integer, good_as_coerce_domain, and good_as_convert_domain all cdef functions that can't be called from Python-space?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Should be fixed now, thanks!

Comment thread src/sage/structure/parent.pyi Outdated
Comment on lines +45 to +46
names: NameSpec = ...,
normalize: bool = ...,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
names: NameSpec = ...,
normalize: bool = ...,
names: NameSpec = None,
normalize: bool = True,

Comment thread src/sage/structure/parent.pyi Outdated
Comment thread src/sage/structure/parent.pyi Outdated
def category(self) -> Category: ...
def _test_category(self, **options: Any) -> None: ...
def _test_eq(self, **options: Any) -> None: ...
def init_coerce(self, warn: bool = ...) -> None: ...
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
def init_coerce(self, warn: bool = ...) -> None: ...

This is a cdef method.

@vincentmacri
Copy link
Copy Markdown
Member

@vincentmacri Thanks for the review, and sorry that it took me so long to come back to you (here and in other PRs).

No worries! I've had plenty of other work to do lately as well.

@vincentmacri
Copy link
Copy Markdown
Member

I think this is s: needs work but perhaps I'm just misunderstanding something, so s: needs info for now.

Comment thread src/sage/categories/homset.py Outdated
from sage.structure.unique_representation import UniqueRepresentation

_cache: TripleDict[SageObject, SageObject, Category | None, "Homset"] = TripleDict(weak_values=True)
_cache: "TripleDict[SageObject, SageObject, Category | None, Homset]" = TripleDict(weak_values=True)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Add from __future__ import annotations to the top of the file and you can remove the quotes here.

Comment on lines +56 to +58
def __mul__(self, right: Map[Any, Any]) -> Map[Any, Any]: ...
def _composition(self, right: Map[Any, Any]) -> Map[Any, Any]: ...
def _composition_(self, right: Map[Any, Any], homset: Any) -> Map[Any, Any]: ...
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Not necessary for this PR, just pointing out that these functions would be could candidates for overloads utilizing generics in the future, i.e. if $f \colon B \to C$ and $g \colon A \to B$ then $f \circ g \colon A \to C$.

@vincentmacri
Copy link
Copy Markdown
Member

There are a couple suggestions I made for default values in parent.pyi that I think you forgot to accept. Otherwise looks good. There are parts that could have more precise types, but this is a great start and we can build upon it in the future. You can set to positive review after adding the suggested values to parent.pyi.

Thanks for the contribution! Once this is merged I think I can finish the type annotations for Integer.

Co-authored-by: Vincent Macri <vincent.macri@ucalgary.ca>
@vincentmacri
Copy link
Copy Markdown
Member

Test failure seems irrelevant.

@tobiasdiez
Copy link
Copy Markdown
Contributor Author

Thanks!

vbraun pushed a commit to vbraun/sage that referenced this pull request Jan 17, 2026
sagemathgh-41232: Add type annotations for rings and parents
    
<!-- ^ Please provide a concise and informative title. -->
<!-- ^ Don't put issue numbers in the title, do this in the PR
description below. -->
<!-- ^ For example, instead of "Fixes sagemath#12345" use "Introduce new method
to calculate 1 + 2". -->
<!-- v Describe your changes below in detail. -->
<!-- v Why is this change required? What problem does it solve? -->
<!-- v If this PR resolves an open issue, please link to it here. For
example, "Fixes sagemath#12345". -->

Add a bit more typing annotations/stubs for a few fundamental objects
(with the goal to make typing of the higher level Python interface
easier). They are not meant to be 100% accurate and contain a few to
many `Any`s for my taste, but it should serve as a good start.

### 📝 Checklist

<!-- Put an `x` in all the boxes that apply. -->

- [ ] The title is concise and informative.
- [ ] The description explains in detail what this PR is about.
- [ ] I have linked a relevant issue or discussion.
- [ ] I have created tests covering the changes.
- [ ] I have updated the documentation and checked the documentation
preview.

### ⌛ Dependencies

<!-- List all open PRs that this PR logically depends on. For example,
-->
<!-- - sagemath#12345: short description why this is a dependency -->
<!-- - sagemath#34567: ... -->
    
URL: sagemath#41232
Reported by: Tobias Diez
Reviewer(s): Tobias Diez, Vincent Macri
vbraun pushed a commit to vbraun/sage that referenced this pull request Jan 19, 2026
sagemathgh-41232: Add type annotations for rings and parents
    
<!-- ^ Please provide a concise and informative title. -->
<!-- ^ Don't put issue numbers in the title, do this in the PR
description below. -->
<!-- ^ For example, instead of "Fixes sagemath#12345" use "Introduce new method
to calculate 1 + 2". -->
<!-- v Describe your changes below in detail. -->
<!-- v Why is this change required? What problem does it solve? -->
<!-- v If this PR resolves an open issue, please link to it here. For
example, "Fixes sagemath#12345". -->

Add a bit more typing annotations/stubs for a few fundamental objects
(with the goal to make typing of the higher level Python interface
easier). They are not meant to be 100% accurate and contain a few to
many `Any`s for my taste, but it should serve as a good start.

### 📝 Checklist

<!-- Put an `x` in all the boxes that apply. -->

- [ ] The title is concise and informative.
- [ ] The description explains in detail what this PR is about.
- [ ] I have linked a relevant issue or discussion.
- [ ] I have created tests covering the changes.
- [ ] I have updated the documentation and checked the documentation
preview.

### ⌛ Dependencies

<!-- List all open PRs that this PR logically depends on. For example,
-->
<!-- - sagemath#12345: short description why this is a dependency -->
<!-- - sagemath#34567: ... -->
    
URL: sagemath#41232
Reported by: Tobias Diez
Reviewer(s): Tobias Diez, Vincent Macri
@vbraun vbraun merged commit ab9bc81 into sagemath:develop Jan 25, 2026
22 of 23 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants