Search code examples
pythonoperating-systempython-typing

How to annotate a variable in Python based on a type that depends on the operating system?


Say one develops an application that should run on Windows and on Linux and the application uses the pyserial package which provides the type serial.serialposix.Serial when installed in Linux and the type serial.serialwin32.Serial when installed in Windows. Is it possible to annotate the variable type, so that the linter knows which type to use based on the OS?

import serial
from serial.serialposix import Serial as PosixSerial
from serial.serialwin32 import Serial as Win32Serial

# linter complains on Windows
serial_port: PosixSerial = serial.Serial('/dev/ttyUSB0')

# linter complains on Linux
serial_port: Win32Serial = serial.Serial('COM1')

# linter complains on both Windows and Linux
serial_port: PosixSerial | Win32Serial = serial.Serial(f"{name}") 

Solution

  • There are some constant literals for python installations, two of them are sys.platform and os.name.

    os.name is more general and will be nt (Windows), posix (linux) or java. (Check out sys.platform if you need fine grained options).

    The following code creates a platform-aware variable Serial, Win32Serial and PosixSerial are TypeAlias that are identical to Serial on their matching platform and Never on the respective other one.

    Your type-checker/linter can pick up these constants and will infer only one if clause as reachable here, hence it will know exactly if Serial is the windows or linux (or another) variant on your current system or if you change the constant for the linter.

    import os
    import serial
    if os.name == "nt":
        from serial.serialwin32 import Serial  # only reachable on Windows
        Win32Serial: TypeAlias = Serial
        PosixSerial = Never
        serial_port: Win32Serial = serial.Serial('COM1')
    elif os.name == "posix":
        from serial.serialposix import Serial  # only reachable on Linux
        PosixSerial: TypeAlias = Serial
        Win32Serial = Never
        serial_port: PosixSerial = serial.Serial('/dev/ttyUSB0')
    else:
       ...  # maybe lint as SerialBase
    
    # Serial is platform-aware, however statically only one covered at the same time
    another_port: Serial = serial.Serial(f"{name}")
    

    The use of Never is optional, using it allows you to check to never reach linux code on windows (an alternative is also to use typing.assert_never). For example:

    def windows_only(a: Win32Serial):
       ...
    
    # Windows OK
    # Posix: error: Type "Serial" is not assignable to type "Win32Serial" (reportArgumentType)
    windows_only(serial_port)