Avoiding surprise ImportError with platform specific symbols

I develop mainly on Linux, and our CI suite then runs on Windows and Linux.

I have pre-commit hooks for the usual stuff, but every now CI will fail with e.g.:

ImportError: cannot import name 'SIGKILL' from 'signal' (C:\hostedtoolcache\windows\Python\3.12.2\x64\Lib\signal.py). Did you mean: 'SIGILL'?

A few searches didn’t turn up anything like a platform specific import linter, is that just because it’s such an edge case, or is there some technical reason it’s hard to implement?

I’d love it if there were some programmatic way to detect platform specific imports that aren’t wrapped e.g. a sys.platform check.

Perhaps this is something typing could help with, since the flow control logic is already baked in?

1 Like

There are already a bunch of sys.platform branches in typeshed, so typing does help with this to a degree if you run the type checker on all the platforms. Both mypy and pyright provide options to change the platform, so you can run these checks all locally on a single platform. You can also test multiple python versions this way.

That being said. This is far from a complete solution and there’s some constants in modules like signal that exist only on very specific platforms, like certain flavors of unix or even just single linux distributions. So it’s difficult to cover absolutely every case with typing, but you should at least be able to catch a reasonable portion of the platform specific errors you can expect.

I also run into this problem from time to time.

My vague thought so far is that - given that I already use Pylance to see squiggles the moment there’s some kind of typing error - I’d like to be able to say to the type-checker something like, “This code should work on both Windows and Linux” and have it error if I use something that only happens to be available on one of them, to remind me to either use something else or add the required platform check to get rid of the error.

On a very similar note, I’d also like to be able to say something like “This code should work on all Python versions between 3.9 and 3.12” and be able to see an error squiggle the moment I use something that’s not available in at least one Python version in the declared range.

I’m not sure how feasible it is to do anything about this, though!

Is continuous integration an option for you? Because doing this statically is very hard.
If you frequently commit, you’ll get an error right away when something is incompatible.

CI is a great tool, but it’s not the right tool for this job. CI’s purpose is to catch problems that you didn’t before your code goes out any further. My post was about improving the editor experience. The OP clearly already uses CI and started this thread as a result of CI giving them an error!

What I’d like is that if I type (for example, and assuming import typing) typing.ParamSpec into an editor, then as soon as I next pause typing for the one or two seconds it takes for code assist to update, I see a red squiggle on what I just typed, with a message that typing.ParamSpec doesn’t exist in Python 3.9 (assuming I’ve previously told it I want to support that version).

To use CI for this, I’d have to commit (which means thinking of a commit message, and adding clutter to the history), push, then wait several minutes, and then check some other user interface (not my editor) to find out what the error is. Which is fine when you’ve just finished spending hours writing some big new module, but it’s not what you want to be doing every few lines of code.

But, as above, I don’t expect this to be particularly easy to accomplish - I’m more just noting that the problem exists and that if a solution was found, it’d be fantastic.

Setting the minimum python version you want to support in your mypy/pyright configuration already goes a long way and will catch most of the version specific problems.

You certainly could extend editor integrations to test all the supported version/platform permutations automatically and emit version/platform specific errors accordingly, but you would be increasing the feedback time substantially, so I don’t think it will be fast enough for most projects.

Once Astral’s type checker written in Rust is production ready I would be more confident, that something like this is feasible in an ergonomic way.

That being said, even once type checkers are fast enough, that continually checking multiple versions and platforms is feasible within your editor/language server, it still would only be a partial solution, since it would not be able to handle packages that should be pinned to a lower/higher version due to the Python version, since you only have the one environment. The same goes for checking multiple platforms. Unless you’re testing in the correct environment the test will always be incomplete to a degree.

1 Like

Checking symbols and signatures seems doable, especially for multiple versions, because you can specify the supported versions in pyproject.toml: requires-python = ">=3.8". Ruff detects a few issues, but not a lot. For platforms, you can’t specify sys.platform (linux, win32, darwin, …).

But checking C code or syntax is a lot harder, so might be best handled by CI.

Can you give an example of what this can detect? Because I’m having trouble coming up with an example.

1 Like

Anything in the standard library that’s only supported in newer versions of Python will be caught that way. Ideally you will also get deprecation warnings for things that will not be supported in an upcoming Python version.

Of course this only handles third party packages if they themselves use sys.version_info branches, it doesn’t handle packages where you would need to install an older version of the package in order to be still supported on that version of python.

OK, so 3 things:

  1. pylance (pyright) and mypy don’t support worktrees, your pyproject.toml must be in the root of your workspace. (Which is why I was confused it wasn’t working).
  2. pythonPlatform = "All" does the opposite what I want, it allows code from all platforms, instead of checking them individually.
  3. they stops understanding code in TYPE_CHECKING blocks. (e.g. Op = dict[str, Any])