Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Union[ListNode, None] vs. Optional[ListNode] vs. Optional['ListNode'] [closed]

It seems we can use the following types for hinting LinkedLists in Python:

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

Which of these should we prefer?

Feel free to share any other relevant insights as well.

Attempt

Here we simply want to merge K sorted LinkedLists:

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)

like image 888
awz Avatar asked Apr 17 '26 09:04

awz


2 Answers

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 by PEP 649 – Deferred Evaluation Of Annotations Using Descriptors, and it's coming "mandatory" in Python 3.14.)

like image 107
ruohola Avatar answered Apr 18 '26 23:04

ruohola


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:
    • | instead of Unions
    • | None instead of Optional
    • None should be last
    • | "a_string" pitfall
    • use a type-checking-block; see all TC001 - TC004
like image 44
Daraan Avatar answered Apr 18 '26 23:04

Daraan



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!