Python type hints are Turing complete
arxiv.org
arxiv.org
When Python 3 came out, annotations were indeed added to the language, but no use case was decided for them. It was an experiment to see what the community would do with them instead of forcing a use case base on what the core devs would think would be useful.
In order to allow this, annotations where permitted to be any legal python expression.
Nowadays, type hints have won: annotations are widely used to add static type checking in Python. But the way the language treat them have not changed much appart from becomming lazy. While mypy cannot understand dynamic Python expressions in them, those are nevertheless legal syntax, and will result in correct metadata at runtime.
Therefore, were the typing system not turing complete, the annotations would have been anyway by nature of being basically an exec().
def f() -> int: return 1
def f() -> (lambda: int)(): return 1
only the first is correctly typed by PEP 484. mypy rejects the second with “error: Invalid type comment or annotation”.That’s the whole idea of static type checking: in return for restricting yourself to a controlled subset of the language that isn’t “too dynamic”, you gain the ability to type-check your code without running it.
The result of this paper is that one can nonetheless abuse subtyping to perform Turing-complete computation in the type checker. The computation doesn’t happen when your code is executed (annotations have no effect on the Python interpreter at runtime unless your program chooses to inspect them), it happens when you run mypy on it.
This is generally considered undesirable, because we want type checking to be decidable, and Turing-complete computation is not.
Eh, you are mixing up distinctions.
All that's required of _static_ typing is that your static type specification finishes running before you start your program.
Whether that static type specification language is Turing complete or not is a completely separate issue. And, yes, you are right that it's generally useful for that language to be decidable. But see eg Haskell's UndecidableInstances.
But thanks to how we can introspect annotations, we could have a specialized syntax for types that would be also usable for those things, while the other way around would be harder.
So it's nice we went the type hints road, because you can now define types, and yet have wonderful libs like pydantic, fastapi, typer, taichi and so on.
I designed typedload before pydantic was a thing and I made it with mypy in mind already, and with the idea of using the standard library, not defining my own dataclass.
Unfortunately mypy has a limitation so you can't make a function that takes Type[T] and returns T. You can define it but it won't work for all types. There is still discussion on how to provide this feature.
@inject
def main(service: Service = Provide[Container.service]) -> None:
...
FastAPI also does a similar sort of trick, for DI and also request parsing.https://python-dependency-injector.ets-labs.org/introduction...
taichi does ffi with their own dsl, cython with c.
I you haven't tried pydantic or sister projects (django-ninja, fastapi and typer), this is a good place to start. They bring a breath of fresh air and are quite fun to use.
The dep injection is fastapi and typer code though, and quite tied to it. So it's worth mentioning that somebody is attempting (quite successfully from the look of it) to create a generic lib of dep injection using annotation named DI, inspired by those libs: https://github.com/adriangb/di
An example:
@dataclass
class HackerNewsPost:
title: str
author: str
votes: int = 0
This will generate a constructor that looks like this: def __init__(title: str, author: str, votes: int = 0):
self.title = title
self.author = author
self.votes = votes
This is pretty useful, writing __init__ boilerplate has been a common thing in my experience using Python, and it makes code more compact and readable.It can do some other things too!
https://cython.readthedocs.io/en/latest/src/tutorial/external.htmlYou define a Python function interface but populate it with a string of C code.
def add(x: int, y: int) -> int:
""" // c code
int x, y
return x + y
"""
And it compiles it and turns it into a C extension?That's...horrifying yet amazing.
It's either xlcalculator or pycel, one of the two. I forgot which one does it that way.
https://github.com/bradbase/xlcalculator#addingregistering-e...
It remained enabled with a future and while it kinda works, it doesn't work in all cases. Non top level definitions can never be retrieved basically.
From this thread [0] it does not look open and shut that it will work once these changes go into effect.
I've seen similar (in-house) to generate HTML forms also.
> For example, every string is an object, `str <: object`, but not every object is a string, `str ≮: object`.
Is not
str <: object ∧ str ≮: object
a contradiction? Wouldn't `object ≮: string` be the correct representation of "not every object is a string"?Would building a DSL in the language give you the same dynamic?
> Would building a DSL in the language give you the same dynamic?
The Lisp people did something like that. I can't find it now, alas.
Doesn't this just cause the same problems? If language B (the type system) is Turing complete you'll start doing whatever you don't like doing in language A (the host) and end up with similar problems you had in A, but now in B.
| but still be adapted to very different purposes.
I guess this is what's needed but each should be constrained as to NOT be able to do certain things.
(Replace the x in arxiv.org with a 5 to get there. I feel like it's working well enough in most cases, when will they start linking these from the actual arxiv article pages?)
screenshot: https://cdn.billmill.org/static/newsyctmp/pytyping.png
> Real computers constructed so far can be functionally analyzed like a single-tape Turing machine (the "tape" corresponding to their memory); thus the associated mathematics can apply by abstracting their operation far enough. However, real computers have limited physical resources, so they are only linear bounded automaton complete. In contrast, a universal computer is defined as a device with a Turing-complete instruction set, infinite memory, and infinite available time.
https://en.m.wikipedia.org/wiki/Turing_completeness#Non-math...
When we say something is Turing complete, there’s always an implicit “if it ran on a computer with unlimited memory” assumption that comes with it, because no computer with limited memory can ever simulate all Turing machines. You can always construct a Touring machine that makes a given computer run out of memory.
So technically, no real computer or computer program is Turing complete, or able to accurately simulate any Turing machine. The whole thing is an irrelevant technicality though, as A) we have so much memory available that we might as well treat it as unlimited and B) touring completeness is a theoretical property - if you prove it for something like python type hints, that proof won’t assume any memory limitations, ie. it’ll assume the computer the thing runs on has infinite memory. The proof is valid even though no such infinite computer actually exists.
Not going to keep replying as this is not really a point I am going to get convinced of - to be technically turing complete is to show that every thing computable by a turing machine is computable by your construct. This is not possible in a memory constrained system.
a turing machine is something way more specific than just "something that can execute algorithms" though, it's a machine that executes them using a specific system and constraints (namely advancing and modifying a strip of tape). So calling anything that can compute what a turing machine can compute a turing machine would be inaccurate just by merit of that.
of course ultimately you're right though, words mean what they're used to mean, so turing completeness excludes the infinite memory constraint, my initial comment wasn't really meant fully seriously, just being a smartass for a joke.
:(){ :|:& };:
It's a ... uh ... a recursive parallel processing algorithm. Yeah.
> Hindley–Milner type inference is DEXPTIME-complete. In fact, merely deciding whether an ML program is typeable (without having to infer a type) is itself DEXPTIME-complete. Non-linear behaviour does manifest itself, yet mostly on pathological inputs. Thus the complexity theoretic proofs by Mairson (1990) and Kfoury, Tiuryn & Urzyczyn (1990) came as a surprise to the research community.
[1] https://en.wikipedia.org/wiki/Hindley%E2%80%93Milner_type_sy...
I’d consider most dependent type systems to be in this category.
See https://en.wikipedia.org/wiki/Intersection_type_discipline
Shen has a 'static' type system which embeds a predicate calculus, thus being trivially Turing-complete, and not by accident.
Why is it down-voted?
I can't get used to the Typing features. It makes the code look overly verbose and needlessly explicit. I love the simplicity and cleanliness of Python code, even at the expense of not specifying type hints and all that new jazz.
Class members, I generally always type. Hmm let me think. That's probably about it.
I extensively use named tuples and dataclasses too which I think also benefit from having as much type info as possible. I think it fills them in nicely given they're already immutable.
My first job - 5 years - was being neck deep in a huge python 2 codebase with dubious historical maintenance and programmer capability track record. I grew to despise enormous List[Dict[Tuple[str, str, str], Tuple[str, str, int]]] except that oopsies! Sometimes that int is a float and sometimes null and sometimes a string and on the last day of the month one of the records will be a custom class that we insert and test for to handle specially.
Also cases where some variable is passed down through multiple call sites without usage and a short name - like "f" - and it's not clear to me if it's a file, an io object, a string holding a filename, or what. I'd like that typed too.
With static analysis they won't deviate anymore. But the price you're paying for this is verbosity, complexity, and constant support of types.
I find Python static analysis incredibly unpythonic.
So before I say this, let's agree nobody will harass anybody, and we will politely disagree on this.
Ok, disclaimer done. Here's what I got to say:
I find Python type hints unsatisfying because they don't do enough. Existing type checkers that use them have limitations, are difficult to use beyond trivial usage, and fail to detect cases which are trivial for statically typed languages. Because of this, you never feel truly safe or covered because you're using them, so writing type hints seems like wasteful effort that is difficult to justify to skeptical Python developers.
However, simply saying "it's not enforced, it's just documentation" is also unsatisfying to me. Computers should work for us; if they can automate and enforce something, we should let them. This feels like the worst of both worlds: it's unenforced, slowly-drifting documentation (so the language does very little work for you) but it lacks the freeform of other documentation styles, and you must follow a formal syntax. And developers don't like writing it, since it does nothing anyway.
Coming from the statically typed world, I struggled to justify type hints to inexperienced devs who saw all the holes in them, and so they simply refused. I had no authority to enforce anything, and in any case, enforcing by mandate is not ideal.
So I wish type hints were better so that my arguments for them could be more solid.
"ok now type g... wait, why isn't get_foo autocompleting for you? Hold on, lets go add a hint..."
I realize that you're not going to piecemeal your way to satisfying mypy output like this, but I think it plants the seed in a way that prevents the mentor from being the type-cops.
Even then, I still fully type all of my code because it lets me write fewer tests.
> Computers should work for us; if they can automate and enforce something, we should let them
I agree. But in my experience type hints are often treated as a goal, not as a low cost solution. "We're not going to compromise on type safety". Same happens in TypeScript ecosystem.
As you correctly mentioned, Python type hints are somewhat limited. So naturally what happens next is people change the way they write code to work around those limitations. Readability and velocity (major things Python is praised for) gets sacrificed for the sake of "type safety".
The promise was for tooling to correct my mistakes. But more often than not I felt that I have to correct tooling mistakes.
Before mypy became popular, I often used type annotations because I wanted to. I didn't use them everywhere, I wouldn't enforce them in code reviews, I would never use deeply nested types (those are just unreadable) or generics. I was able to extract value from type hints, they gave me 80% of value for 20% of effort. Static analysis feels like it tries to get another 10% of value for 90% of cost. It's not a good deal anymore.
I wrote this disclaimer because I was surprised to find harassment for such a harmless topic as Python's type hinting. I'm surprised dang never stepped in. The flamewar went for a surreal nesting depth, and you know what goes on at that level -- nobody cares what the initial disagreement was, we were arguing about the meaning of the word "is" at that point.
I bet it could.
The only way around this would be to set a recursion depth, which changes the semantics.
Rust is doing basically the same thing. Default code is "safe", and the compiler is able to prove its correctness. If the compiler can't make heads or tails of it, you have to put it in "unsafe" - and the programmer is expected to check the correctness herself.
This is also true of Python, but a program which is just `print("hi!")` can be statically proven to halt.
I suspect this is true of a very great majority of type hints in Python which actually exist.
[0]: https://blog.joshuakgoldberg.com/type-system-game-engines/
[1]: https://github.com/jamiebuilds/json-parser-in-typescript-ver...
I always felt like the Python type system was underpowered compared to e.g. TypeScript (not necessarily a bad thing), but I guess it’s still enough to be Turing-complete.
I.e. if the hinting system is turing complete, it surely can express the concept as a program?
The fact that you can embed any arbitrary Turing complete operation into the type checking operation does not mean you have the correct input and output operations to apply a Turing-complete dependent type system onto a Python program.
Now, I'm not saying it can't be done, because I'm not taking a close enough look to be sure, and underestimating motivated type hackers is not a smart idea. But my gut says the odds that you're missing at least one critical source of information or output action you'd need to make this work approaches one. Especially in Python, of all languages. Python's "primitives" are incredibly complex and full of capability and trying to constrain them via dependent types is not entirely dissimilar to trying to make a "safe" subset of Python, which has proved to be effectively impossible even with a great deal more access to the internals of Python over the decades than a type system checking would have.
Church-encoded numerals are a common trick. This basically means the number 3 is encoded as Suc<Suc<Suc<Zero>>>.
Python type checking allows eval with arbitrary code. Of course it's Turing complete.
I'd recommend actually reading the paper.
The paper does that by hooking into some basic results of theoretical computer science: Turing Machines (TMs) can model any program, there is no program that can look at another program and say that it finishes execution (the halting problem is undecidable), and, oops, you can compile TMs into Python type hints, therefore type checking Python is undecidable.
If I understand correctly, that means type-checking python as you described was an open problem before, and this paper effectively proves it will never happen (ideally). They do this by establishing Turing completeness of type-hints, which are the main mechanisms for type-checking.
* https://doc.pypy.org/en/latest/faq.html#would-type-annotatio...
It also means that it's expressive enough such that it can be more powerful then a traditional type system
Turing equivalence seems more interesting. Intuitively I'd think that type systems are supposed to be more constrained than programs they are being applied to. The fact that they are equivalent makes me think that those type systems are flawed (though I can't explain why).
There are plenty of languages with turing complete type systems.
agda, idris, coq, haskell, scala, etc...
I wonder how many people actually do this and whether the proportion is big enough such that much of what's written in comments is heavily distorted.
Although often confused, fairly enough as they were introduced in the same PEP, an annotation is a part of the Python syntax and type hints are a specific set of notations and ways you can use those notations inside an annotation to be used by a type hinter (yes those notations are evaluated to objects at Python runtime but the type hinter never has to be aware of that or even ever use a Python runtime).
The paper is talking about those notations inside a type hinter, not annotation objects inside a Python runtime. e.g. you can get mypy to run a turing complete program using only the notations defined in PEP 484.
What good does it do for the "hints" to do the work of Python, Pascal, C++ or anything else for that matter?
It does "hints". Does it do "hints" well? If yes, then it provides value.
I think it's more: "Watch out, you could create types that never resolve."