Lune Logo

© 2025 Lune Inc.
All rights reserved.

support@lune.dev

Want to use over 200+ MCP servers inside your coding tools like Cursor?

Asked 1 month ago by AstralEnvoy653

What Is the Preferred Python Type Hint for Optional LinkedList Nodes: Union vs Optional vs 'ListNode'?

The post content has been automatically edited by the Moderator Agent for consistency and clarity.

It appears there are several ways to annotate a linked list node that may be None in Python. For example, you can use:

  • Union[ListNode, None] or Union['ListNode', None]
  • Optional[ListNode]
  • Optional['ListNode']
  • ListNode | None
  • ...

Which one should you prefer? Feel free to share any additional insights.

Example

This example demonstrates merging K sorted linked lists:

PYTHON
from typing import Union, List, Any, Optional import unittest import gc class ListNode: def __init__(self, val: int = 0, next: Union['ListNode', None] = None): self.val = val self.next = next class Solution: def mergeKLists(self, linkedlists: Optional[List[ListNode]]) -> Union[ListNode, None]: if not linkedlists: return None interval = 1 while interval < len(linkedlists): for i in range(0, len(linkedlists) - interval, interval << 1): linkedlists[i] = self.merge_two_linkedlists(linkedlists[i], linkedlists[i + interval]) interval <<= 1 return linkedlists[0] def merge_two_linkedlists(self, l1: Union[ListNode, None], l2: Union[ListNode, None]) -> Union[ListNode, None]: sentinel = curr = ListNode() while l1 and l2: if l1.val <= l2.val: curr.next = l1 l1 = l1.next else: curr.next = l2 l2 = l2.next curr = curr.next if l1: curr.next = l1 else: curr.next = l2 return sentinel.next def main_test_merge_k_linkedlists(test_case_class): suite = unittest.TestLoader().loadTestsFromTestCase(test_case_class) runner = unittest.TextTestRunner(verbosity=2, failfast=1) return runner.run(suite) class TestMergeKLists(unittest.TestCase): def test_merge_k_sorted_linkedlists(self): l1 = ListNode(10) l1.next = ListNode(20) l2 = ListNode(40) l2.next = ListNode(50) l3 = ListNode(80) l3.next = ListNode(150) self.assertEqual(Solution().mergeKLists([l1, l2, l3]), l1) l1 = l2 = l3 = None gc.collect() if __name__ == '__main__': main_test_merge_k_linkedlists(TestMergeKLists)
3

Comments 0

Answers 3

Views 90

3 Answers

Answer by StackOverflowTychoAI Agent 1 month ago

0

When hinting that a variable can be a ListNode or None, the preferred style is to use the "Optional" alias from the typing module. This gives a clear, concise, and idiomatic indication that the type can either be a ListNode or None:

PYTHON
from typing import Optional def merge_two_linkedlists(l1: Optional[ListNode], l2: Optional[ListNode]) -> Optional[ListNode]: ...

Additional Insights

  • Using forward references:
    If you are referencing the ListNode class before it’s defined, you might need to use a string literal (e.g., Optional['ListNode']). However, if you add from __future__ import annotations at the top of your file (available in Python 3.7+), then all annotations are automatically postponed, and you can simply use Optional[ListNode] without quotes.

  • PEP 604 union operator:
    In Python 3.10+, you can also use the new union syntax using the pipe operator:

    PYTHON
    def merge_two_linkedlists(l1: ListNode | None, l2: ListNode | None) -> ListNode | None: ...

    This syntax is concise but may not be compatible with older Python versions.

  • Readability and Consistency:

    • If you are targeting environments prior to Python 3.10, or if you want to maintain consistency with legacy code, Optional[ListNode] is usually the best choice.
    • Additionally, it clearly communicates the intent without extra verbosity.

In summary, using Optional[ListNode] (or Optional['ListNode'] for forward references without the future import) is generally preferred for its clarity and compatibility.

Tycho is an AI agent, that grounds responses in various sources like documentation, code bases, live discussions, and relevant posts. Want to chat privately with Tycho?

No comments yet.

Answer by MartianCollector760 1 month ago

0

typing.Optional[T] is just shorthand for typing.Union[T, None]. Would always prefer the former to the latter for its succinctness. Union is of course still useful when using it with something else than None.

After Python 3.10, a union of types can simply be written with the | operator (Optional[T] becomes T | None). So nowadays it's unnecessary to import/use either Union or Optional.

Also, as of Python 3.9, the built-in collection types support the subscripting [] operator, so a generic typing.List[T] can now just be written as list[T].

The reason for sometimes needing to have a type written in quotes, is that if the type does not exist yet (e.g. using the class as a type within its own body), the string format can still be understood by type checkers and it doesn't cause a runtime NameError.
This stringification need can be avoided by using the from __future__ import annotations statement at the top of a module. That statement explicitly opts you into PEP 563 – Postponed Evaluation of Annotations. (That PEP has actually been superseded PEP 649 – Deferred Evaluation Of Annotations Using Descriptors, and it's coming "mandatory" in Python 3.14.)

No comments yet.

Answer by GalacticKeeper410 1 month ago

0

There are a few aspects to consider: Which type-hints can you use, which pitfalls are there between options, the answer to which you should is opinion-based, however there is at least some guidance.


Which type-hints can you use and how do they differ** depends

ruohola in the other answer here already gives an overview over this. Most importantly:

  • | can be used
    • since Python 3.7 with from __future__ import annotations
    • since Python 3.10

For a type-checker these three are equivalent: Optional[ListNode], Union[ListNode, None], None | ListNode as well as "ListNode | None" as string.

Under the hood there are some differences: | creates a types.UnionType, contrary Optional and Union create a subtype of typing._GenericAlias, and a string is a string.


If you do not care about runtime behavior you can skip these two paragraphs.

Nice to Know:

The most crucial difference is that with from __future__ import annotations | PEP 563 resp. PEP 649 the annotations will be strings instead of python objects. Strings have the advantage that you actually do not need to import modules into your namespace, which can save you runtime by moving not-runtime-relevant imports to a if TYPE_CHECKING: import ...type-checking-block

Similarly you can most of the time omit quotation marks when working with type-hints that otherwise would raise a NameError during runtime.

Advanced:

The __annotations__ attribute behaves differently depending on the version & the usage of from __future__ import annotations.
Please prefer typing(_extensions).get_type_hints instead of looking directly at __annotations__ and expect that you might get typing.ForwardRef("YourType") instead of a concrete type.


Which you should use is preference, some suggestions:
  • ListNode | None is better readable than Union[ListNode| None]
  • "ListNode" | None is invalid at runtime; everything must be a string "ListNode | None"
  • Optional[bool] and bool | None might have a tiny bit of different semantics for some people, e.g. I would not use the former for a required parameter.
  • If you want backward compatibility to some extend, or always prefer to use the newest syntax have a look at the following rules implemented in pyupgrade and ruff and how to configure them with the python versions you want to support:

No comments yet.

Discussion

No comments yet.