Search code examples
pythonpython-3.6typecheckingmypy

Is it possible to separate all type hinting / checking infrastructure in Python into .pyi files?


With Python 3, type hinting allows for third party tools like mypy to check types; however, I find the type checking syntax to be a bit ugly alongside program comments. With mypy it's possible to put stub functions and methods into separate .pyi files in the same directory as the .py files. Is it possible to define all type related syntax in the equivalent .pyi file for a .py file? So, for example, you could be able to define variable, method, return types, etc. in the .pyi file with all the type syntax, but leave the main .py code alone?


Solution

  • Not really. While PEP 484 does specify that you can create custom pyi files that end up acting as the "public interface" for your existing modules for the benefit of any users of your code, it doesn't specify a way to take those pyi files and internally typecheck the corresponding py file.

    In fact, if a directory contains both a py and pyi file, the py file will be ignored completely.

    There's an open feature request for this on the mypy issue tracker, but it seems like a low-priority kind of task to me because even if that feature were implemented, you'd ultimately be able to use only a subset of Python's type system.

    For example, suppose we wanted to use something like NewType to write some code for templating HTML or something:

    from pathlib import Path
    from typing import NewType
    
    UnsanitizedText = NewType('UnsanitizedText', str)
    CleanHtml = NewType('CleanHtml', str)
    HtmlTemplate = NewType('HtmlTemplate', str)
    
    def get_user_input() -> UnsanitizedText:
        # code omitted
    
    def escape_to_html(raw: UnsanitizedText) -> CleanHtml:
        # perform checks on 'raw', do escaping logic, etc
        return CleanHtml(cleaned_string)
    
    def load_template(path: Path) -> HtmlTemplate:
        return HtmlTemplate(path.read_text())
    
    def render_template(template: HtmlTemplate, *kwargs: CleanHtml) -> CleanHtml:
        # code omitted
    
    t = load_template(Path("foo/bar.html"))
    dirty = get_user_input()
    clean = escape_to_html(dirty)
    
    print(render_template(t, arg=dirty))  # Does not typecheck
    print(render_template(t, arg=clean))  # Typechecks
    

    Since we actually do need to call the phantom newtypes we constructed/can mix and mingle these phantom calls within regular expressions, it's unclear how we would represent this module as a stub.

    There are several other typing features that would also be difficult to represent in stubs -- things like casts, NamedTuple, TypedDict, etc.