1. 17
  1. 3

    I think you could use importlib instead of import and predeclare the module variable with the appropriate type-hinting. I don’t know if that’s preferable, the HAS_MARKDOWN flag is reminiscent of C “include once” macros. I haven’t tried this, but it might “look” nicer.

    1. 2

      Unfortunately this prevents Mypy from loading the type hints for the imported module.

      1. 1

        Ah, darn.

    2. 3

      A better way to handle this is to pull the things out of the module that you need in the try statement. For example:

      try:
        import markdown
        md_render = markdown.render
      except:
        md_render = lambda _: ""
      

      Then just use md_render without any conditionality. That’s how I’ve always done it (even before mypy), and it typechecks under mypy just fine.

      1. 1

        For this approach to work, you need to align the type hints of your dummy methods with the imported object type hints. This is normally impractical. For example for the actual markdown rendering method it takes a dummy function like so:

        from typing import Any, Sequence, Union, Mapping, Literal
        
        try:
            import markdown
        
            render_markdown = markdown.markdown
        except ImportError:
        
            def render_markdown(
                text: str,
                extensions: Union[
                    Sequence[Union[str, markdown.extensions.Extension]], None
                ] = None,
                extension_configs=Union[Mapping[str, Mapping[str, Any]], None],
                output_format=Union[Literal["xhtml"], Literal["html"], None],
                tab_length=Union[int, None],
            ) -> str:
                return text
        
        
        reveal_type(render_markdown)
        

        Eek :o

        1. 1

          Doesn’t the first render_markdown = markdown.markdown fix the type of render_markdown (from mypy’s perspective), and then the second one can just be e.g. def render_markdown(text, *args): return text without any loss of typing?

          edit: Yep, that’s exactly what happens. More specifically this works:

          try:
              import markdown
          
              render_markdown = markdown.markdown
          except ImportError:
              def render_markdown(
                  text: str, *args, **kwargs,
              ) -> str:
                  return text
          
          reveal_type(render_markdown)
          

          and reveal_type outputs:

          type.py:17: note: Revealed type is 'def (text: builtins.str, *, extensions: Union[typing.Sequence[Union[builtins.str, markdown.extensions.Extension]], None] =, extension_configs: Union[typing.Mapping[builtins.str, typing.Mapping[builtins.str, Any]], None] =, output_format: Union[Literal['xhtml'], Literal['html'], None] =, tab_length: Union[builtins.int, None] =) -> builtins.str'
          
          1. 1

            Okay yes, that works. Still a bit longer than adding a HAVE_MARKDOWN bool :)

            One small tweak: you should run with --strict, or at least --disallow-untyped-defs. Doing so will disallow skipping the types on *args, **kwargs, but then you can use Any for them :

            from typing import Any
            
            try:
                import markdown
            
                render_markdown = markdown.markdown
            except ImportError:
            
                def render_markdown(text: str, *args: Any, **kwargs: Any) -> str:
                    return text
            
            
            reveal_type(render_markdown)
            

            I blogged about typing *args, **kwargs here: https://adamj.eu/tech/2021/05/11/python-type-hints-args-and-kwargs/

      2. 3

        There is an increasing trend of perverting the “natural” Python code in order to conform to mypy’s idea of correct code, which is strongly informed by the limitations of their static-analysis code. Personally, I think it’s a mistake. Write your code for humans, not for linters.

        1. 2

          This is just a beautiful little blog post – a practical problem personal to the author (but shared by many), a practical solution, concisely written, entertainingly told, I loved it. It feels like one of those many small knowledge-building and -disseminating posts from the heyday of programming weblogs dropped through time to be here, with us, in the year 2021.

          1. 2

            Thanks 😊

          2. 1

            pytype does handle this properly, though with an amusing quirk in the type display:

            $ cat test.py 
            try:
                import markdown
            except ImportError:
                markdown = None
            
            if markdown is not None:
                a = markdown.foo
            
            b = markdown.foo
            
            $ pytype test.py 
            Computing dependencies
            Analyzing 1 sources with 0 local dependencies
            ninja: Entering directory `.pytype'
            [1/1] check test
            FAILED: /home/mdemello/tmp/.pytype/pyi/test.pyi 
            /home/mdemello/.asdf/installs/python/3.7.7/bin/python3.7 -m pytype.single --imports_info /home/mdemello/tmp/.pytype/imports/test.imports --module-name test -V 3.7 -o /home/mdemello/tmp/.pytype/pyi/test.pyi --analyze-annotated --fix-module-collisions --nofail --quick /home/mdemello/tmp/test.py
            File "/home/mdemello/tmp/test.py", line 7, in <module>: No attribute 'foo' on module 'markdown' [module-attr]
            File "/home/mdemello/tmp/test.py", line 9, in <module>: No attribute 'foo' on None [attribute-error]
              In Optional[import markdown]
            File "/home/mdemello/tmp/test.py", line 9, in <module>: No attribute 'foo' on module 'markdown' [module-attr]