Hacker News new | past | comments | ask | show | jobs | submit login
Type hints – a mediocre programmer's reaction (2015) (python.org)
106 points by ingve on March 17, 2016 | hide | past | favorite | 54 comments



That's what you get from duck typing.

Python API are defined solely by how the API uses its argument. The requirement is already encoded as far as the computer is concerned, except the check are by default all done at run-time. A Python compiler could work its way backward to try to verify them at compilation time. Making them explicit would solely be as a helper for the programmer, which might as well just be in straight documentation, which can ba as brief as the name of the parameter.

It's all a feature, and this is being actively exploited in code, providing exceptional flexibility. Its no worse than other language with type inference: their types can be extremely complex and reading them would not help most of the time. You're better off with a succint, high-level description, like a URL-like string, a file-like object, an object indexable by such and such.


Well, the problem is on annotating types, when you only actually care about behavior.

Duck typing isn't a bad thing. In fact, it's great. You just need to annotate the ducks instead of the types. For a great example of how to do it, look at Haskell's type classes.

(In fact, Haskell's easyness of integrating generic code is visible even to a beginner, but I never notice that this is because the entire language is based on duck typing.)


To be pedantic, Haskell technically doesn't use duck typing, it uses structural typing. Duck typing is run-time coercion of types, structural typing is compile-time inference of minimum interfaces.


Noticed.

Interface based typing may be a better generic term.


Yeah, I immediately thought of Rust traits, which I hear are very like typeclasses. Python already has something like these, in the form of Abstract Base Classes: collections.Callable for function-like things, collections.Iterable for things that conform to the iterator protocol... But if gradual typing is to be useful, it really needs ABCs for all the common ducks that Python code uses: file-like objects, things that dict() can convert into a dictionary, things that can be converted into strings...


I'm not sure any committee could agree as to what defines a filelike object (does it implement seek?)

In addition you'd need an IRO (interface resolution order) if you truly intended to prove everything would work

To say nothing of accidentally implementing an interface and getting an error because if it

Doesn't sound like my kind of python...


You have a seekable filelike interface that is a superset of the base filelike interface.


>I'm not sure any committee could agree as to what defines a filelike object (does it implement seek?)

You step back a layer of abstraction. Maybe it implements seek, maybe it doesn't. Either way it implements has_seek.


Well... either your library invents its own type classes, and all client code must implement them (boilerplate!). Or it relies on agreed-upon type classes which come from another library. If it's not basic classes like Functor or Monad that's just not manageable (dependency hell!).


Dependency hell is the situation where A depends on both B and C, but B depends on D, and C depends on D', where D and D' are not compatible.

Type classes absolutely do not create this kind of situation, as they are duck typing. But modules do create it, so it happens both on Haskell and on Python.

Now, in practice you use both, basic classes and library specific ones. And yes, if you want to create some behavior, you'll need to implement the specific code that enables it. You do not escape from that in Python either.


I know dependency hell when I see it. It's already bothersome if I can't just apt-get install the right library version because another package depends on a conflicting version, or the latest boost version isn't available yet. I refuse to learn tools like pip or stack for every language I use, and to create sandboxes for new projects only to get the latest library versions. That's just never going to be portable software.

You can actually escape dependency hell for most APIs. That's the Unix culture of using plain text (read: very simple data types -- in C that would be arrays and structures of integral types) for communication.

Of course that won't work for libraries that provide transformations on parameterized structures. But let's face it, there's no point in depending on a Monad library just to save two lines of pure code.


"Duck typing" can be done statically through row polymorphism. For more info, read up on OCaml's object system.


You only need row polymorphism if you want full type inference. You can get away with width subtyping with bounded parametric polymorphism if you are willing to give up on inferring all types. Examples: Go, TypeScript, Flow...


as we totally accept two-element lists here as well as two-tuples.

This is a little a symptom of lack of overloading (and lack of static typing therefore), but should be not hard to solve buy disjunctive hints like this one: [a]|(a,b) (and in case of Python, looks like Union[] would do?).

The URL case is also pretty much the same and for example in Java you would have to write a different method signature for each such case and that would be natural. Again, disjunctive hints would solve this.

Given that, I do not know what is the complaint really here. Perhaps that syntax is horrible, or it is hard to know what are the actual types in Python?


This is totally crazy. With a slightly different syntax it's really not so bad:

{a:b} for mappings from a to b

(a,b) for tuples of a and b

[a] for iterables of a

a | b for unions of a and b

Then factor out that nasty common thing and you end up with:

    Optional[    {basestring: StringThing}
              | [(basestring, StringThing)]
            ]

    StringThing = (basestring, Optional[basestring | file]])
                | (basestring, Optional[basestring | file]], Optional[basestring])
                | (basestring, Optional[basestring | file]], Optional[basestring], Optional[Headers])    

    Headers =  {basestring: basestring}
            | [(basestring, basestring)]
It's not inherently complicated nor does it have anything to do with duck typing, it's just that the syntax chosen is awful.


Couldn't they use helper methods to convert all those types he brought up to a single common type that they annotated with? It would simplify the API a lot. I come from a full day of working with associated types, generics, type erasures and protocol extensions, there is a darker side to it.

I wanted to have a generic protocol that could have some logic of its own without enforcing you to subclass it. I would regret the decision so much, if I wasn't busy regretting other decisions even more.

Just enforce a type that you accept that's large enough to be used by many and convertable from other common types you want to use.

I swear, soon they'll classify programmers urge to write as generic and extendable as a mental illness.


> Couldn't they use helper methods to convert all those types he brought up to a single common type that they annotated with?

But then it wouldn't be "pythonic". To be frank though, I think being too generous with your inputs is just asking for maintenance hell. I don't see why requests feels the need to allow data in so many different formats when one or two + conversion functions for anything else would be sufficient.


In my (limited) Python experience, this kind of amorphous-blob-of-properties interface is great fun to write but is absolute hell to decipher later, especially if you're not the person who wrote it. You end up having to read the entire function and usually recurse through most of the things it calls in order to even know how to call it. That's maybe not so much of an issue if you're just writing small self-contained scripts, but an interface to an API it's awful.


I've come to realize it's an addiction/urge, that's the only /reasonable/ answer I can give.


https://en.m.wikipedia.org/wiki/Robustness_principle

""" Be conservative in what you do, be liberal in what you accept from others (often reworded as "Be conservative in what you send, be liberal in what you accept"). """


Didn't we decide later that that's a bad idea? I mean, in the early days of the web, Internet Explorer was the very embodiment of this principle and a couple of decades later the internet is still full of the resulting non-standard crud.


Yes we did. It can also lead to really surprising security issues in protocol-land where such "liberalism" interacts badly with security-sensitive portions of protocols.

(Security is non-trivial and mostly there are very good reasons that things are exactly as they are in such protocols.)


First reply is a very practical response:

> At this point, you may want to just stop caring about the exact type.

> Part of the point of gradual typing is that you can short-cut a lot of

> this. And quite frankly, this isn't really helping anything. Just skip

> it and say that it's Union[Mapping, Iterable, None].


And the reply to this reply shows the problem:

"This change doesn't catch any of the bugs anyone was going to hit (no-one is passing a callable to this parameter), and it doesn't catch any of the bugs someone might actually hit (getting the tuple elements in the wrong order, for example). At this point the type signature is worse than useless."


The public interface of a library isn't where type hints will shine, and I'm not sure why someone would think they would.

Type hints are going to be awesome for nailing down the behavior of the core parts of your business application. I think of it more like writing good tests than anything else.

Like everything else in development; 80% of the problem is choosing the right tool for the job.


This. Types, except for those built-in to the language, are not for APIs. But they can be great for internals if you don't overdo it.

For example, Perl6 has Subset (types), which is a great feature. You could make a subset type of Str (a built-in type) which contains only strings that match a given pattern (e.g. URLs, user-ids, or shell-safe strings) and still benefit from all generic string code.


I think there is such option in Python ABCs.


But having every library have type hints means your entire program could be provable. Business logic doesn't make up very much of most applications (at least, not as much as the libraries it's built on).

You're right, though. Python doesn't seem to be made for applications that would benefit from being provably compiled.


Thank god I'm not the only one who feels that way about PEP 0484. No offense to Guido or the other authors, but I don't know what was going on in their minds.


Some PEPs are just wrong. This is one of them. It's kowtowing to dumb editors when the point of the language is to be as human-friendly as possible. If we wanted an IDE-friendly language we'd just use Java.


The general atmosphere in programming communities has been that dynamically typed languages are just wrong and everything should be statically typed.

This has probably been driven by languages that showed that static typing doesn't have to be as inconvenient as Java, like Haskell or Rust, but the proponents also seem to argue for inferior type systems.


This isn't just the problem of 'duck-typing' vs 'strong typing.' There is always going to be data whose shape is very hard to define and annotate explicitly with intent of machine type checking or verification, yet much easier to describe to a programmer.

Take for example a spec for a "Set-Cookie" header syntax. It can be described as "https://tools.ietf.org/html/rfc6265#section-4.1.1", which would be a rather unwieldy thing to show, looking similarly to the pythonic polymorphic argument described in this message.

Type hinting (and strong typing) helps machines 100% of the time, and users some of the time. It's helpful if your API has an interface that is easy for a human to understand, even if you describe your data as 'a URI', "truthy" or even "Any", even if their shapes are not strictly defined.


> 'duck-typing' vs 'strong typing.'

For the n'th time, python is a strong typed AND duck typed language. Did you mean static?


Yes, it's going to be hard to retrofit Python libraries that use this style of parameter passing.

But if you're starting fresh, it's not that hard to use keyword parameters to support a lot of different options while also having reasonable types. (Take a look at how Dart libraries do it.)


Why can't Python have transpilers like JS does? Adding types to Python like Typescript could be a good thing.


I feel like JS mainly has transpilers because people are locked in to the language, which is not true of python


i'm pretty sure js transpilers are a symptom of having to support many js environments and js versions but folks wanting to have access to current language features anyway. people have made this too for python and it's called `3to2`, which is a backwards version of the `2to3` language upgrade tool.

babeljs for example used to be called `6to5` so i don't think it's lockin that causes this desire, but it's certainly true that the options for clientside programming languages that are widely deployed are fairly slim. i don't think they're related though.


People that choose Python are "locked in" to a language that doesn't have static typing.


You are missing the point. People that choose Python do just that, they choose Python. The use of JS is very often not a matter of choice, because it's the only language supported in browsers.


Exactly, there is a difference between torture (JavaScript) and masochism (Python).


People that choose Python _chose_ a language that doesn't have static typing.

People that want to write code that runs in a Web browser are locked in to Javascript, so they're "locked in" to JS; I can't think of a platform where the only option is to use Python.


Mods for Civ IV? :P


Obviously old and feature-full libraries aren't going to benefit much from type-hinting. More than anything, I see it as a way to encourage new projects to re-think how they design their APIs - i.e. keeping it simple. That huge polymorphic argument example just seems like convenience was picked over API design by the Requests authors, and if you decide on that order of priority, type-hints aren't going to be useful to you.


> That huge polymorphic argument example just seems like convenience was picked over API design by the Requests authors

Convenience is API design. In a fully dynamically-typed language, there's no real downside for heavily polymorphic APIs and a lot of upside in terms of brevity and flexibility.

I believe pretty strongly that you can design great dynamically-typed APIs for dynamically-typed languages or great statically-typed APIs for statically-typed ones, but I don't think you can easily layer static types on a great dynamic API. The affordances are just too different.

One obvious challenge is that statically typed languages almost always support overloading. This makes it dramatically easier to have these kinds of polymorphic APIs while having sane types. Most dynamically typed language support overloading at all, which is where these giant union types come from when you try to cram a single static type onto what is effectively a number of different method overloads.


> In a fully dynamically-typed language, there's no real downside for heavily polymorphic APIs and a lot of upside in terms of brevity and flexibility.

Wow, I'd say that's a major pain point for me—reasoning about polymorphic code with only vague guidelines:

"give me something that looks like a string".

"Well, which types are those?"

"Hell if I know! But it needs these methods, but it can also change its behavior if it doesn't have them"

"stabs self in neck because this was solved in the 60s"

Seeing a hard-to-skim polymorphic API for anything but a quick script is a massive problem for me—the time sink in just parsing its behavior isn't worth the inevitable problems that come with the edge cases at runtime.

That said, requests is great and I use it to do all my web scraping because it's super idiomatic. I had no idea the interface was that complicated, though, and it reduces my confidence in the library.

This PEP reminds me of typed racket: pretty awesome, but a completely separate language to write with separate considerations.


Requests is or has been the most downloaded egg from pypi. If you have used it, you'll know why: it makes dealing with http requests a pure pleasure. Dead easy to remember how to use it, and dead easy to read it. So discouraging this kind of library seems misguided.


But you really use all the polymorfic alternatives the library gives you? I expect the 90% are just simple cases, and the rest can be factored out in specific API


Python isn't about types, it is about protocols. If you are thinking about the type of something while writing the code, then you are transcoding C++ or Java into Python. The important thing about a Python instance is not the type, but the protocols that the instance supplies.

Switching gears and thinking about code validation for a minute, I agree, protocol conformance checking is a much more difficult problem to resolve at compile time, at least they way Python duck types support protocol definitions.


Aren't protocols just a particular kind of abstract type? In Swift, "interfaces" or "traits" are literally called protocols.


It's objective-c terminology originally. Interfaces, traits, protocols are all fairly interchangeable. Objective-c already used @interface to declare the public method list of a concrete type, so it's a little odd in this case.


If you go that route, anything can be abstract type.

I tend to see it like: type is what this object actually is, and protocol is how we interact with it.


I see where you are going. But I'm sure we can agree that both concrete types and protocols would be handled by a "type checker".

If ever there were a language where you'd want to be looking at protocols and not concrete types most of the time, it would be Python.


Type hints for Python would be useful if used like structural typing from Prismatic's Schema for Clojure [1], where you can pattern match against tuples/lists/maps/keys but also against arbitrary predicate functions (such that you could do `instanceof('foo')` or `hasattr('foo')`).

[1] https://github.com/plumatic/schema


In for example JavaScript or other high level programming language: Instead of implementing type check/hints, your functions should validate the input data and throw an error if it doesn't meet the requirements. The compiler/VM should chose the optimal type. Types are for low level systems programming. In high level programming, think form design and validation instead of types. Example: Argument "yearBorn" should take a numeric value of two or four numbers. Not a "unsigned short int".




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: