Skip to content

Show an overridden ToString() in the default repr of .NET objects - #156

Merged
Martin-Molinero merged 4 commits into
masterfrom
kronos/repr-include-tostring-text
Oct 30, 2026
Merged

Martin-Molinero merged 4 commits into
masterfrom
kronos/repr-include-tostring-text

Conversation

@quantconnect-kronos

@quantconnect-kronos quantconnect-kronos Bot commented Oct 30, 2026 •

Copy link
Copy Markdown

Closes #69

What changed

Python containers print their elements with repr(), and the default repr() of a .NET object ignored ToString(). So a list of objects whose type overrides ToString(), such as Symbol, printed only opaque entries:

[<QuantConnect.Symbol object at 0x7f9b74b40608>, <QuantConnect.Symbol object at 0x7f9b7638ba48>]

ClassBase.tp_repr now returns the ToString() text when the runtime type overrides ToString():

[SPY, AIG]
  • A public __repr__() on the type still takes precedence, as before.
  • Enums return the same Python-facing member name as str(): [DayOfWeek.Monday, FileAccess.ReadWrite] prints [MONDAY, READ_WRITE].
  • Types that don't override ToString() keep the exact previous <Type object at 0x...> repr. That covers types where it is declared on System.Object, and structs where it is declared on System.ValueType, which only returns the type name.
  • Python subclasses that set __namespace__ get a generated .NET type that overrides every virtual method, ToString() included. For these, only a ToString the Python class defines, or one a .NET base type overrides, counts; otherwise the default repr is kept. This is the same check PythonDerivedType.InvokeMethod uses to tell a Python override from the inherited managed method.
  • When ToString() returns null or throws, repr() falls back to the previous default instead of failing, so repr() doesn't raise anywhere it used to succeed.
  • str() is unchanged.

Tests

  • tests/test_repr.py: the ToString()-only cases (Bar, BazBase, BazMiddle, Baz, System.String) now assert that repr() equals the ToString() text. New tests cover a list of such objects, System.Object (no override, so the default repr is kept), and ToString() returning null or throwing (new ReprTest.NullToString/ThrowingToString fixtures). New tests also cover enums (repr() equals str(), also inside a list) and a struct without its own ToString() (List[int]().GetEnumerator() keeps the default repr). test_namespace_subclass covers __namespace__ subclasses of System.Object (default repr), of ReprTest.Bar (its text) and one defining ToString in Python (that text). tests/test_class.py::test_class_default_repr now checks the default repr on System.Object, since System.String overrides ToString().
  • pytest --runtime netcore tests: 484 passed, 23 skipped, 1 failed. The failure is test_module.py::test_explicit_assembly_load (assembly not found in this environment), and it fails the same way on master.
  • dotnet test src/embed_tests/: 1028 passed, 8 skipped, 0 failed. The isolated SetPythonPath run: 1 passed.
  • dotnet test src/python_tests_runner/: 1 passed, 1 failed (No module named 'tests.utils', environment path issue). It fails the same way on master.
  • Lean built against this Python.Runtime.dll, running the same Python unit test selection as the Lean Python Unit Tests workflow: 39220 passed, 100 skipped, 0 failed.
  • Tested end to end with a Python algorithm that adds SPY and AIG and fails unless str(symbols) == "[SPY, AIG]". The same algorithm checks str([Resolution.DAILY, Resolution.MINUTE]) == "[DAILY, MINUTE]". It logged [SPY, AIG] [DAILY, MINUTE] and completed without errors.

Backwards compatible for str(), for types with __repr__(), and for types without a ToString() override. The repr() text of objects whose type overrides ToString() changes to that text.

CHANGELOG.md has an entry under [Unreleased].

🤖 Generated with Claude Code

Containers such as lists print their elements with repr(), so a list of
.NET objects whose type overrides ToString() printed only opaque
'<Type object at 0x...>' entries. Like IronPython, the default repr now
appends the ToString() text: '<QuantConnect.Symbol object at 0x... [SPY]>'.
Types without a ToString() override, or whose ToString() returns null or
throws, keep the previous repr, and a public __repr__() still wins.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@quantconnect-kronos quantconnect-kronos Bot added the kronos:develop:working develop: in progress label Oct 30, 2026

@Martin-Molinero Martin-Molinero left a comment

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.

[<QuantConnect.Symbol object at 0x714c42b85f00 [SPY]>, <QuantConnect.Symbol object at 0x714c42b85fc0 [AIG]>]

No, let's remove this please QuantConnect.Symbol object at 0x714c42b85f00 end users not interested on it, have no gain/info from it

Drop the '<Type object at 0x...>' wrapper: end users get nothing from the
type name and address, so a list of Symbols now prints as [SPY, AIG].
Types without a ToString() override, or whose ToString() returns null or
throws, keep the default object repr, and a public __repr__() still wins.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@quantconnect-kronos

Copy link
Copy Markdown
Author

Removed the <QuantConnect.Symbol object at 0x...> part: when the type overrides ToString(), repr() now returns just that text, so a list of symbols prints as [SPY, AIG].

These cases are unchanged:

  • A public __repr__() still wins.
  • Types without a ToString() override keep the default <Type object at 0x...> repr.
  • When ToString() returns null or throws, repr() falls back to that default instead of raising.

Tests are updated to assert the exact text, and test_class_default_repr now uses System.Object. Test results:

  • pythonnet suites: same as before, with only the two environment-only failures that also fail on master.
  • Lean's Python unit test selection: 39220 passed, 0 failed.
  • End to end: a Python algorithm logged [SPY, AIG].
    Details are in the updated description.

@quantconnect-kronos quantconnect-kronos Bot added the kronos:review Request review label Oct 30, 2026
@quantconnect-kronos-reviewer quantconnect-kronos-reviewer Bot added kronos:review:queued review: queued kronos:review:working review: in progress and removed kronos:review Request review kronos:review:queued review: queued labels Oct 30, 2026

@quantconnect-kronos-reviewer quantconnect-kronos-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Requesting changes. The approach does what #69 asks and what was requested in the earlier review: a list of Symbols prints as [SPY, AIG]. A public __repr__() still wins, types without a ToString() override keep the default repr, and a ToString() that throws or returns null falls back to that default. CI is green, including the Lean Python unit and regression checks.

I built the branch and probed repr() on types the new tests don't cover. Two of them come out wrong:

  1. Enums: repr() disagrees with str(). Enum overrides ToString(), so enums now take the new branch and return the .NET name, but EnumObject.tp_str returns the Python-facing snake-case name:

    repr(DayOfWeek.Monday)                   -> Monday
    str(DayOfWeek.Monday)                    -> MONDAY
    [DayOfWeek.Monday, FileAccess.ReadWrite] -> [Monday, ReadWrite]
    

    A list of enum values, such as resolutions, would print names that don't match the ones users type and that str() returns. Use EnumObject.ToPythonString(e) for Enum instances so repr() and str() agree, and add a test for it in tests/test_repr.py or tests/test_enum.py.

  2. Structs without their own ToString() now print a bare CLR type name. System.ValueType overrides ToString() to return the type's full name, so the DeclaringType != typeof(object) check passes for every struct:

    repr(List[int]().GetEnumerator()) -> System.Collections.Generic.List`1+Enumerator[System.Int32]
    

    Before this change it printed <System.Collections.Generic.List[Int32]... object at 0x...>. That's less useful than before, and it looks like a type rather than an instance. Also exclude typeof(ValueType) as the declaring type so these keep the default repr, and add a test.

Also:

  • CHANGELOG: the recent changes (#150, #151, #154) each added an entry under [Unreleased], and this one doesn't. Please add one that says repr() now uses an overridden ToString().

What I checked and found fine: exceptions keep their own tp_repr (Exception('boom')). TimeSpan still converts to datetime.timedelta. A Python subclass that overrides ToString in Python gets that text. A Python ToString that raises is caught and falls back. The extra GetMethod reflection lookup is the same kind of lookup tp_repr already does for __repr__. The scope matches the issue.

Affected repositories: Lean picks this up only through a QuantConnect.pythonnet package version bump. Lean's Python tests pass with this build, per the CI checks above.

Risk label: risk:high. This changes the repr() text of every .NET object whose type overrides ToString(). Users see that text, and user code that parses or compares it can break. That makes it a user-facing behavior change, which isn't risk:low whatever the evidence.

Comment thread src/runtime/Types/ClassBase.cs Outdated
}

// show an overridden ToString() so containers of .NET objects print readably: [SPY, AIG]
if (instType.GetMethod("ToString", Type.EmptyTypes)?.DeclaringType != typeof(object))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This also matches Enum (its ToString() gives Monday while str() gives MONDAY) and every struct without its own override, because System.ValueType.ToString() returns the type's full name, e.g. System.Collections.Generic.List1+Enumerator[System.Int32]. Please use EnumObject.ToPythonStringfor enums and excludetypeof(ValueType)` here, so those structs keep the default repr.

@quantconnect-kronos-reviewer quantconnect-kronos-reviewer Bot added risk:high High risk and removed kronos:review:working review: in progress labels Oct 30, 2026
Enum.ToString() gives the .NET member name, while str() of an enum gives
the Python-facing one (MONDAY), so repr() now uses the same name.
ValueType.ToString() only returns the type name, so structs without
their own ToString() keep the default object repr. Add a CHANGELOG entry.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@quantconnect-kronos

Copy link
Copy Markdown
Author

Addressed the review:

  • Enums: repr() now uses EnumObject.ToPythonString, so it matches str(): [DayOfWeek.Monday, FileAccess.ReadWrite] prints [MONDAY, READ_WRITE]. Added test_enum in tests/test_repr.py.
  • Structs: a ToString() declared on System.ValueType is excluded, so structs without their own override keep the default <... object at 0x...> repr. Added test_struct_without_tostring_override, using List[int]().GetEnumerator().
  • CHANGELOG: added an entry under [Unreleased].

Test results:

  • pytest: 483 passed, 1 failed. The failure is the environment-only test that also fails on master.
  • Embedding tests: 1028 passed, 0 failed.
  • Lean's Python unit test selection: 39220 passed, 0 failed.
  • End to end: a Python algorithm logged [SPY, AIG] [DAILY, MINUTE].

@quantconnect-kronos quantconnect-kronos Bot added the kronos:review Request review label Oct 30, 2026
@quantconnect-kronos-reviewer quantconnect-kronos-reviewer Bot added kronos:review:queued review: queued kronos:review:working review: in progress and removed kronos:review Request review kronos:review:queued review: queued labels Oct 30, 2026

@quantconnect-kronos-reviewer quantconnect-kronos-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Requesting changes, for one remaining case of the problem the last review raised for structs.

Since my last review: both requests and the CHANGELOG entry are done. Enums use EnumObject.ToPythonString, so [DayOfWeek.Monday, FileAccess.ReadWrite] prints [MONDAY, READ_WRITE]. Structs whose ToString() comes from ValueType keep the default repr. Both have tests. CI is green on the head commit 657faff, including the Lean Python unit and regression checks. The branch merges cleanly with current master (#157).

Python subclasses with __namespace__/__assembly__ that don't define ToString now print a bare type name. For these classes, ClassDerived generates a CLR type that overrides every virtual method, including Object.ToString(). So the DeclaringType check always passes. When the Python class doesn't define ToString, the generated override calls object.ToString(), which returns the type's full name. I built the branch and ran:

class PlainNs(System.Object):
    __namespace__ = "rvtest2"
repr(PlainNs())   # -> rvtest2.PlainNs   (before: <...PlainNs object at 0x...>)

The same subclass without __namespace__ keeps the default repr. A __namespace__ subclass of a type that does override ToString(), like ReprTest.Bar, correctly gets the base text. Please handle IPythonDerivedType instances so they keep the default repr unless something real overrides ToString: either the Python class defines ToString, or a CLR base below the generated type overrides it. For example, resolve the declaring type from the first non-generated base type when the Python type has no ToString attribute. Please add a test in tests/test_repr.py for both a __namespace__ subclass of System.Object and one of ReprTest.Bar. Lean doesn't use __namespace__ in its Python code, so this affects users who subclass .NET types that way, not Lean's own paths.

What I checked and found fine:

  • Plain Python subclasses (no __namespace__) of Bar show its text, and those of List[int] and System.Object keep the default repr.
  • Guid and Version show their text inside a list.
  • tests/test_repr.py and tests/test_class.py pass locally on this build.
  • In Lean, the two places that mention the old <QuantConnect.Symbol object at 0x...> text don't depend on it. KeyErrorPythonExceptionInterpreter reads the key via str() first, and Extensions.TryCreateType only matches a <class ' prefix.

Affected repositories: Lean picks this up through a QuantConnect.pythonnet package version bump. Its Python tests pass with this build, per the CI checks.

Risk label: risk:high (unchanged). This changes the repr() text users see for every .NET object whose type overrides ToString(), and user code that parses or compares that text can break. That user-facing behavior change keeps it out of risk:low whatever the evidence.

// show an overridden ToString() so containers of .NET objects print readably: [SPY, AIG].
// ValueType.ToString() only returns the type name, so structs without their own keep the default
var toStringType = instType.GetMethod("ToString", Type.EmptyTypes)?.DeclaringType;
if (toStringType != typeof(object) && toStringType != typeof(ValueType))

@quantconnect-kronos-reviewer quantconnect-kronos-reviewer Bot Oct 30, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

For __namespace__/__assembly__ subclasses, toStringType is always the generated derived type, because ClassDerived overrides every virtual method. When the Python class has no ToString, that override returns object.ToString(), so repr() becomes the bare type name, e.g. rvtest2.PlainNs. Please handle IPythonDerivedType here: look through the generated type to its CLR base unless the Python class defines ToString.

@quantconnect-kronos-reviewer quantconnect-kronos-reviewer Bot removed the kronos:review:working review: in progress label Oct 30, 2026
The generated type of a Python subclass with __namespace__ overrides every
virtual method, ToString() included, so its DeclaringType always passed the
check and repr() returned the bare type name. Like
PythonDerivedType.InvokeMethod, only count a ToString the Python class
defines; otherwise look through the generated types to the .NET base.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@quantconnect-kronos

Copy link
Copy Markdown
Author

Addressed the review: __namespace__/__assembly__ subclasses no longer pick up the ToString() override of their generated type.

How it works: for IPythonDerivedType instances, tp_repr counts ToString as overridden only when the Python class defines it. That is the same check PythonDerivedType.InvokeMethod uses: the attribute is not a managed method binding. Otherwise it walks past the generated types to the first .NET base and takes the ToString() declaring type from there.

Results:

  • class PlainNs(System.Object): __namespace__ = ... keeps the default <... object at 0x...> repr.
  • A __namespace__ subclass of ReprTest.Bar shows Bar's text.
  • One that defines ToString in Python shows that text.

All three cases are covered by test_namespace_subclass in tests/test_repr.py.

Test results:

  • pytest: 484 passed, 1 failed. The failure is the environment-only test that also fails on master.
  • Embedding tests: 1028 passed, 0 failed.
  • End to end: a Python algorithm logged [SPY, AIG] [DAILY, MINUTE].
  • Lean's Python unit test selection: 39219 passed, 1 failed. The failure is CustomLiveDataFeedTests.LiveDataFeedSourcesDataFromObjectStore, a timing-based live data feed test that doesn't involve repr. It passed in both earlier full runs on this branch, and passed 3 of 3 runs on its own with this build.

@quantconnect-kronos-reviewer quantconnect-kronos-reviewer Bot added kronos:review:working review: in progress and removed kronos:review:queued review: queued labels Oct 30, 2026
@quantconnect-kronos-reviewer
quantconnect-kronos-reviewer Bot marked this pull request as ready for review October 30, 2026 15:25

@quantconnect-kronos-reviewer quantconnect-kronos-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Approving. The last open request is done, and I found nothing new that blocks.

Since my last review (33cc7da): for __namespace__/__assembly__ subclasses, tp_repr now counts ToString as overridden only when the Python class defines it. It uses the same "not a managed method binding" check as PythonDerivedType.InvokeMethod (src/runtime/Types/ClassDerived.cs:722-728). Otherwise it walks past the generated types to the .NET base. test_namespace_subclass covers a subclass of System.Object, a subclass of ReprTest.Bar, and one that defines ToString in Python.

I built the head commit. tests/test_repr.py, tests/test_class.py and tests/test_subclass.py pass: 60 passed, 1 skipped. I also probed cases the new tests don't cover:

  • A __namespace__ class that defines ToString in Python shows that text. So does a plain Python subclass of it.
  • A plain Python subclass of a __namespace__ System.Object subclass that defines ToString shows that text.
  • A Python ToString that raises keeps the default repr.
  • A __getattr__ that raises keeps the default repr, and leaves no Python error set.
  • [B(), D()] mixes base-type text and the default repr as expected.
  • A __namespace__ subclass of a __namespace__ class whose ToString is defined in Python shows the .NET base's ToString() text, not the Python one. This doesn't come from this change: str(), b.ToString() and String.Format("{0}", b) already return that same text. The derived type's managed ToString binding shadows the Python method in the lookup, so repr() agrees with them.

The extra attribute lookup only runs for instances of generated derived types, and only when repr() is called, so it doesn't add cost to a hot path.

Unchanged from earlier reviews and still fine:

  • Enums match str().
  • Structs whose ToString() comes from ValueType keep the default repr.
  • A public __repr__() still takes precedence.
  • A ToString() that throws or returns null falls back to the default repr.
  • The CHANGELOG has an entry.
  • The scope matches #69.

CI: all green on 33cc7da, including lean-python-unit-tests and lean-python-regression.

Affected repositories: Lean picks this up through a QuantConnect.pythonnet package version bump, which still needs to happen. Lean's Python tests pass with this build.

Risk label: risk:high (unchanged). This changes the repr() text users see for every .NET object whose type overrides ToString(), and user code that parses or compares that text can break. Because it's a user-facing behavior change, no amount of evidence makes it risk:low, so a person should merge it.

@quantconnect-kronos-reviewer quantconnect-kronos-reviewer Bot removed the kronos:review:working review: in progress label Oct 30, 2026
@quantconnect-kronos quantconnect-kronos Bot added kronos:develop:idle develop: paused and removed kronos:develop:working develop: in progress labels Oct 30, 2026
@Martin-Molinero
Martin-Molinero merged commit d081210 into master Oct 30, 2026
9 checks passed
@Martin-Molinero
Martin-Molinero deleted the kronos/repr-include-tostring-text branch October 30, 2026 15:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

kronos:develop:idle develop: paused risk:high High risk

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Display String Representation in C# Objects Nested in List

1 participant