Search code examples
pythonpython-typing

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


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)


Solution

  • 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.)