Python type hints may not be not for me in practice
utcc.utoronto.ca
utcc.utoronto.ca
They say "...so I can't create a bunch of different names for eg typing.Any and then expect type checkers to complain if I mix them."
`MyType = NewType('MyType', Any)`
is how you do this.
At the end, they suggest a workflow: "I think my ideal type hint situation would be if I could create distinct but otherwise unconstrained types for things like function arguments and function returns, have mypy or other typing tools complain when I mixed them, and then later go back to fill in the concrete implementation details of each type hint"
That's just doing the above, but then changing the `NewType('MyType', Any)` to something like `NewType('MyType', list[dict[str, int]])` later when you want to fill in the concrete implementation.
Mind that support for methods as first-class objects was introduced with Python 2.5 [0].
That was in 2006, a few years before "meth" became engrained in European popular culture as an abbreviation for methamphetamines. The abbreviation was never changed.
[0]: https://docs.python.org/release/2.5/lib/typesmethods.html
(I'm the author of the linked-to article.)
I think most developers who revisit their projects 6+ months later would disagree with the second part of this statement.
My typical flow for "quick scripts" is:
on first pass I'll add basic type hints (typing ":str" after a func param takes .2 seconds)
for more complex data structures (think a json response from an api), dict (or typing.Dict) work fine
if you want a Python project to be maintainable, type hints are a requirement imho.
One of the reason I use typing is IDE completion and error highlighting. For that purpose - I make sure to annotate even the obvious primitive types and for API responses, I find that defining a Pydantic model works very well.
It takes a few seconds to prompt and then check it.
From the GitHub page:
RightTyper is a Python tool that generates types for your function arguments and return values. RightTyper lets your code run at nearly full speed with almost no memory overhead. As a result, you won't experience slow downs in your code or large memory consumption while using it, allowing you to integrate it with your standard tests and development process. By virtue of its design, and in a significant departure from previous approaches, RightTyper only captures the most commonly used types, letting a type checker like mypy detect possibly incorrect type mismatches in your code.
We've all since left, and eBay's OSPO has churned too. Some years ago, I ended up with a giant undismissible banner on every GitHub page that the org we made has been "flagged." Apparently someone at modern eBay saw that we used the eBay logo and threw a trademark fit about it.
GitHub's support is perhaps the most useless I've ever encountered. They take months to reply to an issue, and there's no notification system. They expect you to constantly check your issue to see if they've replied, and they'll close it if you don't reply to their arbitrarily timed messages promptly.
GitHub Support basically told me to fuck myself. They didn't care that it was sanctioned by eBay at the time we made it. They didn't care that I showed them the obsolete eBay OSPO repo that linked to our org. They gave me no avenue to talk to anyone to get it resolved, nor did they give me any way to dismiss the banner.
Unless I want to write a Chrome extension to dismiss it, my GH session will forever have a black mark that I took a contract at vintage eBay, that modern eBay forgot about and sicked itself on.
We also use Python in some places, including the shitty Python type-system (and some cool hackery to make SQLAlchemy feel very typed and work nicely with Pydantic).
Sounds interesting. Can you elaborate on the cool hackery? We introduced SQLModel recently but struggle in a few cases (e.g. multi-level joins). Do you know reference projects for SQLAlchemy and pydantic?
def c(prop: t.Any) -> sa.Column: # type: ignore
return prop
To make it possible to access sqlmodel properties as columns for doing things like `in_` but still maintaining type safety.Added types ourselves to the base model like this:
__table__: t.ClassVar[sa.Table]
Added functions that help with typing like this: @classmethod
async def _fetch_one(cls: t.Type[BaseT], db: BaseReadOnlySqlSession, query: Select) -> t.Optional[BaseT]:
try:
return (await db.execute(query)).scalar_one()
except NoResultFound:
return None
and stuff like this for relationships: def ezrelationship(
model: t.Type[T_],
id_our: t.Union[str, sa.Column], # type: ignore
id_other: t.Optional[t.Union[t.Any, sa.Column]] = None, # type: ignore
) -> T_:
if id_other is None:
id_other = model.id
return sqlm.Relationship(sa_relationship=relationship(model, primaryjoin=f"foreign({id_our}) == {id_other}"))
def ezrelationship_back(
id_our: t.Union[str, sa.Column], # type: ignore
id_other: t.Union[str, sa.Column], # type: ignore
) -> t.Any:
model, only_id2 = id_other.split(".")
return sqlm.Relationship(
sa_relationship=relationship(
model,
primaryjoin=f"foreign({id_our}) == {id_other}_id",
back_populates=only_id2,
)
)
I hope this helps, I don't have time to find all the stuff, but we also hacked on SQLAlchemy a bit, and in other places.The issue very much is a lack of a standard for the entire language; rather than it not being possible.
To my eyes, the problem of choosing useful defaults for complicated types/datastructures is independent of whether I add type hints for them.
I think I am missing something...
To me, this means I don't really understand the python type hinting at all, as adding hints to just one or two functions provides no value to me at all.
I assume I must be not using them usefully, as I've tried adding type hints to some projects and they just seemed to do nothing useful.
https://docs.pydantic.dev/latest/concepts/validation_decorat...
See either beartype [1] or typeguard [2]. And if you're doing any kind of array-based programming (JAX or not), then jaxtyping [3].
[1] https://github.com/beartype/beartype/
The other side is those people who do not find those kind of bugs annoying, or they simply don't get hit by such bugs at a rate that is high enough to warrant using a strong type system. Developers who spend their time prototyping in ipython also get less out of the strong types. The bugs that those developers are concerned about are design bugs, like finding out why a bunch of small async programs reading from a message buss may stall once every second Friday, and where the bug may be a dependency of a dependency of a dependency that do not use a socket timeout. Types are similar not going to help those who spend the wast majority of time on bugs where someone finally says "This design could never have worked".
Anyhow, no need to rehash the same arguments, there was a long thread here on HN about the post, you can read some of it here: https://news.ycombinator.com/item?id=37764326
I've read that F# has units, Ada and Pascal have ranges as types (my understanding is these are runtime enforced mostly), Rust will land const generics that might be useful for matrix type stuff some time soon. Does any language support all 3 of these things well together? Do you basically need fully dependent types for this?
Obviously, with discipline you can work to enforce all these things at runtime, but I'd like it if there was a language that made all 3 of these things straightforward.
Matrix dimensions are certainly doable, for example, because templates representing mathematical types like matrices and vectors can be parametrised by integers defining their dimension(s) as well as the type of an individual element.
You can also use template wizardry to write libraries like mp-units¹ or units² that provide explicit representations for numerical values with units. You can even get fancy with user-defined literals so you can write things like 0.5_m and have a suitably-typed value created (though that particular trick does get less useful once you need arbitrary compound units like kg·m·s⁻²).
Both of those are fairly well-defined problems, and the available solutions do provide a good degree of static checking at compile time.
IMHO, the range question is the trickiest one of your three examples, because in real mathematical code there are so many different things you might want to constrain. You could define a parametrised type representing open or closed ranges of integers between X and Y easily enough, but how far down the rabbit hole do you go? Fractional values with attached precision/error metadata? The 572 specific varieties of matrix that get defined in a linear algebra textbook, and which variety you get back when you compute a product of any two of them?
A lot of my frustration it is that the ergonomics of these things tend to be not great even when they are available. Or the different pieces (units, shape checking, ranges) don't necessarily compose together easily because they end up as 3 separate libraries or something.
And then there's a subtlety where units might be preserved, but x may be "absolute" where as (x - x) is relative and you can do operations with relative units you can't with absolute units and vice versa. Like the difference between x being a position on a map and delta_x being movement from a position. You can subtract two positions on a map in a standard mathematical sense but not add them.
Anecdotally, I find these are the same people who work less effectively and efficiently. At my company, I know people who mainly use Notepad++ for editing code when VSCode (or another IDE) is readily available, who use print over debuggers, who don't get frustrated by runtime errors that could be caught in IDEs, and who opt out of using coding assistants. I happen to know as a matter of fact that the person who codes in Notepad++ frequently has trivial errors, and generally these people don't push code out as fast they could.
And they don't care to change the way they work even after seeing the alternatives and knowing they are objectively more efficient.
I am not their managers, so I say to myself "this is none of my business" and move on. I do feel pity for them.
Anecdotally, I was just writing a generic BPE implementation, and spend a few hours tracking down a bug. I used debug statements to look at the values of expressions, and noticed that something was off. Only later did I figure out that I modified a value, but used the old copy — a simple logic error that #[must_use] could have prevented. cargo clippy -W pedantic is annoying, but this taught be I better listen to what it has to say.
Well, one of my coworkers pushes code quite fast, and also he is the one who get rejected more often because he keep adding .tmp, .pyc and even .env files to his commits. I guess "git add asterisk" is faster, and thus more efficient, than adding files slowly or taking time to edit gitignore.
Not so long ago I read a history here in HN about a guy that first coded in his head, then wrote everything in paper, and finally coded in a computer. It compiled without errors. Slow pusher? Inefficient?
I've read and heard stories about these folks too, apparently this was more common decades ago.
To be clear, I don't think I could pull it off with any language. It's quite impressive and admirable to get things right on the first try.
Having said that, the thing is, languages were a lot simpler back then too. I'm not convinced this is realistically even possible with today's languages unless you constrain yourself to some overly restrictive subset. Like try this with C++, and I would be shocked if you can write nontrivial programs without getting compiler errors. Like to give a trivial example, every time I write my own iterator class for a container, I miss something when I hit compile: like either a comparison operator, or subtraction, or conversion to const iterator, or post-decrement, or subscript, or some member typedef. Or try it with python, and I bet you'll call .get() on something and then forget to check for null somewhere.
I would love to be proven wrong though. If anyone knows of someone who does this with a modern language, please share.
Head, paper, keyboard is what we did in the 80s when compilers were too slow to afford throwing code at them and fix the errors later. Was that code in the HN story a substantial piece of code or some 100 lines program? Our programs used to be small.
Also you can have .env in the .gitignore, yet someone create their file as .env.local and escape the .gitignore pattern. It's easy to come after and lecturing about creating a better .gitignore pattern, but it's even easier to at the very least take a little care of your commits even if it means slower speeds.
And lo and behold, they end up with _more_ design bugs. And the sad part is that they will never even recognize that too much typing is to blame.
Also I would say type hints sacrifice aesthetics, not readability. Most code with type hints is easier to read, in the same way that graphs with labelled axes and units are easier to read. They might have more "stuff" there which people might think is ugly, but they convey critical information which allows you to understand the code.
That has not been my experience in the past few years.
I've always been a fan of type hints in Python: intention behind them was to contribute to readability and when developer had that intention in mind, they worked really well.
However, with the release of mypy and Typescript, engineering culture largely shifted towards "typing is a virtue" mindset. Type hints are no longer a documentation tool, they are a constraint enforcing tool. And that tool is often at odds with readability.
Readability is subjective and ephemeral, type constraints (and intellisense) are very tangible. Naturally, developers are failing to find balance between the two.
I’m working with a medium size python program at the moment. It’s mostly written by someone smart but early career, and they’ve made a rabbit warren of classes and mixins that get combined in complex ways. I’ve been encouraging him to add types - and wherever those types exist, the code becomes 100% more legible to my code editor - and ultimately to me.
I don’t think I’d bother with types in Python for small programs. But my experience is that good type hints lay out a welcome mat to anyone who comes along later to figure the code out. And honestly, a lot of the time that person is the original author, just months or years after the code was written.
This is provably wrong. See https://peps.python.org/pep-3107/#use-cases
> Documentation for parameters and return values ([23])
> Let IDEs show what types a function expects and returns ([16])
> For example, one library might use string-based annotations to provide improved help messages, like so:
def compile(source: "something compilable",
filename: "where the compilable thing comes from",
mode: "is this a single statement or a suite?"):People are sacrificing this when they start using python in the first place
I use type hint press dot button get auto completes
And typing JSON-like data is possible with TypedDict[1].
[0] https://docs.python.org/3/library/dataclasses.html
[1] https://docs.python.org/3/library/typing.html#typing.TypedDi...
How old is your Python, though? TypedDict is from 3.8. That was 5 years ago.
> @dataclass(frozen=True)
to create an immutable data class. @dataclass(frozen=True)
class Foo:
bar: int
baz: str
@classmethod
def new(cls, bar: int) -> "Foo":
baz = calculate_baz(bar)
return cls(bar, baz)
foo = Foo.new(10)To me, namedtuples are a convenience to give a nicer syntax than ordinary tuples in scenarios where I don't want the overhead of having to store a copy of all the keys with every object, like a dict would. Dataclass seems to be even more stuff on top of a class which is effectively even more stuff on top of a dict, but all the use cases of namedtuples are those where you want much less stuff than an ordinary class has. And I don't want to have to define a custom class just as I often don't define a custom namedtuple in my code but use the one the database driver generates based on the query, which is a very common use case for namedtuples as efficient temporary storage of data that then gets processed to something else.
It's in that funny position though where it is in danger of becoming synonymous with Flutter. Like Ruby and Rails.
Isn't it strange that back when Python (or Ruby) didn't even have type hints (not type checkers, type hints!), it would easily outperform pretty much every heavily typed language?
Somehow when types weren't an option we weren't going towards the cliff, but now that they are, not using them means jumping off a cliff? Something doesn't add up.
There's also a larger understanding that as programs get larger and larger, they get harder to maintain and more importantly refactor, and good types help with this much more than brittle unit tests do. (You can also eliminate a lot of busywork tests with types.)
A certain generation of devs thought types were academic nonsense and then relearned the existence of those features in other languages. Now they are zealots about using them.
We’ve come a long way from the C++ or Java I wrote when I was young, where types were named and renamed constantly. As I understand it, even C++ has the auto keyword now.
#include <string>
auto func(auto x, auto y) -> auto {
return x + y;
}
auto main() -> int {
auto i = func(1, 2);
auto s = func(std::string("a"), std::string("b"));
}
`int` is required as a return type from `main`, but everything else is inferred. This works because `func` becomes a template function where each parameter type is a separate template type, so you get compile-time duck typing. It also works with concepts (e.g. `std::integral auto x`).It's quite neat, but I don't think anyone actually writes code this way, except for lambdas.
Erm yes we were. Untyped Python wasn't magically tolerable just because type hints hadn't been implemented yet.
https://charliereese.ca/y-combinator-top-50-software-startup...
Startups are also more likely to do monoliths.
For Enterprise & microservices, you will start to see more Java/Go/C#.
Regardless, to me enterprise represents legacy, bureaucracy, incidental complexity, heavy typing, stagnation.
I understand that some people would like to think that heavy type-reliance is a way for enterprise to address some of it's inherent problems.
But I personally believe that it's just another symptom of enterprise mindset. Long-ass upfront design documents and "designing the layout of the program in types first" are clearly of the same nature.
It's no surprise that Typescript was born at Microsoft.
You want your company to stagnate sooner? Hyperfixate on types. Now your startup can feel the "joys" of enterprise even at the seed stage.
The real enterprise death doesn’t come from types. It comes from tasteless over use of classes - especially once you have a complex web of long lived objects that and all reference each other. Significant portions of code in these codebases ends up dedicated to useless tasks like lifecycle management instead of the actual work of your application. It’s kind of the code version of corporate beaurocracy - classes everywhere devoted to doing BS jobs.
It’s not complicated people. Just write the code that tells the computer what you want it to do. No more. Unnecessary encapsulation and premature abstraction will kill your velocity dead.
Besides nobody is claiming that it's impossible to build a successful products with dynamic typing. It's just not as good. You can build a successful product with zero comments in your codebase, doesn't mean it's a good idea.
Again, the evidence (as limited as it is) suggests otherwise. You are more likely to succeed if you're going with dynamic language and not doing "proper engineering". This has been widely accepted before type-checker era, and I see no reason why it would be different now. Utilize type checker when it's free, but don't waste time on type puzzles.
"Proper engineering" doesn't get you to product-market fit faster. All it does is tickle your ego.
Inserting a library that wraps an existing one to add new features has been a nightmare in every statically typed language I’ve used — including times it’s virtually impossible because you’d need the underlying library to understand the wrapper type in its methods.
In Python (with duck typing), that’s a complete non-issue.
I see your point - I certainly find myself reaching for clever high level patterns less in typescript than I do in JavaScript because complex typing can get in the way. But also, programs that make heavy use of metaprogramming are often, also, harder to read and debug. There’s something very nice and straightforward about explicit, concrete types.
I used a HTTP requests library in a nuxtjs app (probably nuxt's native library) and I spent too much of my time conjuring the request and response types that would please the type checker. It was extremely frustrating because the code would work in Javascript but the compiler wouldn't accept it because of typing.
I can't give you the details because I'm not at my computer now but the type was a mix of HTTP verbs and the structure of the JSON response. I gave up after a while and rewrote the code using fetch and no types. If they stand between me and the final result they can go down the drain.
It doesn't happen too often, but its definitely annoying.
In cases like this, the easiest way is to just add as any to your expression - which essentially turns off type checking for that expression. Maybe that's what you did?
fetch('foo.json', {...} as any)
I don't think there's anything wrong with this. Using typescript types for only 95% of your code rather than 100% still provides a lot of value in my opinion.You can also ctrl+click on functions like this and read the actual types they're expecting.
const response = await fetch(
url,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(reqData),
}
);
const resData: ApiResponse = await response.json();
then I parsed resData. The JSON in the response is still type checked but I don't have to fight anymore with the HTTP library. I can't remember what it was as it never made it into a commit.It can be slightly laborious to manually wrap a bunch of operations so you can override something, but it's more of an annoyance/inefficiency than something that adds cognitive overhead. That said, many languages (eg structurally typed ones like TS) it should be a non-issue.
No it didn't. It outperformed Java 1.2, and people thought that Java 1.2 was what a typed language looked like. Python always sucked compared to OCaml (yet alone OCaml with a decent IDE), but OCaml had a weird syntax and the documentation was in French, so no-one cared. Now that we finally have a copy of OCaml with curly braces and a critical mass of obnoxious fanboy hype, more people have noticed.
Coming from the perspective of a religious python hater, their type hints are better than what you give credit for: Supports generics, nominative, structural, unions, bottom type, and literals.
What is missing is mainstream adoption in libraries which is a matter of time.
I don't think that's a big problem anymore. Between typeshed and typing's overall momentum, most libraries have at least decent typing and those that don't often have typed alternatives.
ORMs have entered the chat…
These sometimes use a lot of dynamic modification, such as adding implicit ID fields or adding properties to navigate a relationship with another type that is defined in code only from the other side.
It can also be awkward to deal with “not null” database fields if the way the ORM model classes are defined means fields are nullable as far as the Python type hints are concerned, yet the results of an actual database query should never have a null value there. Guarding against None every time you refer to one of them is tedious.
I’m not exactly the world’s loudest advocate for ORMs anyway, but on projects that also try to take type safety seriously, they do seem to be a bit of a dark corner within the Python ecosystem.
with sqlalchemy mapped_column, its less of an issue. django, otoh, seems too much magic for static type. (happy to be proven wrong).
The Rust Evangelism Strike Force used to be more subtle! (joke)
I don't know how other IDEs behave, but VScode + the Python extensions try to infer the missing hints and you end up with beauties such as `str | None | Any | Unknown`, which of course are completely meaningless.
Even worse, the IDE marks as an error some code that is perfectly correct, because it somehow doesn't match those nonsensical hints. And so it gives you the worst of both worlds: a lot of false positives that you quickly learn to ignore, dooming the few actual type errors to irrelevance, because you'll ignore them anyways until they blow up at runtime, just as it'd happen without typehints.
I'd reconsider that if I was doing more than the odd bug fix on the project. I still like Python, and started using type hints early, but there's enough added friction to make me question using them in the future.
I imagine on big projects the benefit is clearer.
Asking because I was really, really annoyed by the non-helpfulness of the type hints in practice, contrary to the theory.
But maybe if you have a codebase with a lot of magic of certain libraries, you experience is different. I also don't really depend on the typing treat it the same as C# or Java.
If VSCode still highlights it, then it is time to configure VSCode properly.
Whenever I find myself in that situation, I usually write a typing stub for the parts that I use from that library (example: [0]) and then let `mypy_path` point to that directory [1].
VS Code will then pick up the hints from those stubs.
[0]: https://github.com/claui/itchcraft/blob/5ca04e070f56bf794c38...
[1]: https://github.com/claui/itchcraft/blob/5ca04e070f56bf794c38...
Are they correct? If they're correct (even though they are a superset of the actual intended type) then what's the problem?
At worst, it's like not having type checks for that particular package.
I usually don't think of None as a potential return value (= voids in C) but the LSP code analysis usually picks up on code paths that don't return a value.
I don't find Python's typing valuable for Jupyter type explorations, but they're immensely valuable for catching little issues in production code.
I'm missing a lot simple functions with explicit argument names and docstrings with arguments types and descriptions clearly but discreetly documented.
It was one big strength of Python to have so simple and clean code without too much boilerplate.
Also, I have the feeling that static typing extremist are trying to push the idea that type hinting allows to ensure to not mix types as it would be bad. But from my point of view the polymorphic and typing mixing aspect is a strong force of Python.
Like having dictionaries that are able to hold whatever you want is so incredible when you compare to trying to do the equivalent in Java for example.
One part where I find type hint to be wonderful still is for things like pydantic and dataclasses!
Can't you just make a dictionary of objects, same as in C#? Except that in C#, if you really want to, you can also use `dynamic` to get python-like behavior.
Otherwise, generally speaking, in a strongly typed language you want to figure out what those objects have in common and put that inside an interface. If you can't modify those objects just slap an adapter pattern on top.
The result is a dictionary of objects that adhere to a specific interface, which defines all the properties and procedures that are relevant to the domain and the problem.
This makes thinking about the problem much easier from a type theoretical perspective because it lets you abstract away the concrete details of each object while preserving the fundamental aspects that you care about.
I guess that it takes two different mindsets to in order to appreciate the pros and cons of dynamic vs static programming. There are certainly many pros for dynamic programming, but I'm more comfortable thinking about problems in generic terms where every relation and constraint is laid bare in front of me, one level removed from the actual implementation.
What type annotations do however deliver is useful completion via LSP.
Compared to legacy Python, yes.
Compared to verbose language like Java, no. Python typing is equal or less verbose than Java (unless you use "var" in Java).
def func(data, *kwargs): """data: the data. kwargs: other data."""
Python: def foo(x: int, y: int) -> int: return x + y
Java: int foo(int x, int y) { return x + y; }
Python uses colons and arrows while Java uses positions to encode where the type should go. > I think that type hint are making python source code messy and unreadable.
I hear this sentiment a lot from people who rarely use strict(er) typed languages: Rust, C++, Java, C#, Go, etc. Can you imagine a developer in any of those languages complaining that "oh, now the code is messy and unreadable because we added explicit types"? It seems bizarre to think about it. Sure, Java and C# is a bit repetitive, but at least you always know the type.There is an ongoing debate in C++, Java, and C# if the newish keyword "auto"/"var" is a good idea to hide local variable explicit types. The real issue: For the person who wrote the code, they already know the implicit types.. However, for people reading the code, they have a harder time to understand the implicit types.
Python used to be described as "executable pseudocode". None of the languages you've listed have ever been considered that easy to read.
Making Python look more like them is therefore a step backwards in terms of cleanliness and readability.
And that's never been true once you're past toy examples into real-world programs. I agree that type hints are "ugly" to read, but they make the code much easier to understand. You have to be aware of the types anyway, and it's better to have them explicit in the code than in your head.
You can still have duck typing with Protocols if you like that kind of thing. Explicitly saying what you expect the incoming type to be able to do is much better than reading the code and having to track all the attributes and methods so you know what you can use.
If we need to make changes to the DB at work, I’ll just update the prisma schema and run ‘npx prisma generate’ followed by ‘tsc —noEmit’ to instantly see all the affected areas. I feel like there are a lot of similar little superpowers you get by having a nice static type system.
- The LLMs can really help with typing tricky situations. If your editor can't already tell you what to use, asking an LLM usually can give me the answer.
- Type annotations for code that might change is a lifesaver, because when I change it later on I now get a bunch of conflicts where I've used it using the old way.
- Feel free to add annotations where it makes sense and is easy, and if something doesn't make sense or it is too hard to figure out the right type, you can skip it and still gain the benefits of using it elsewhere.
- Annotations don't "force you to think about types", you already are thinking about types. They let you think a bit less about types I would argue, because they're documented in function calls and returns. "Can I read() from input_file, or do I need to open()read()?" "input_file:Path" makes it better documented, without encoding the object type in the name.
I'm coming up on 30 years of using Python, and I never really missed typing, but honestly I write basically all of my new code with annotations because of the IDE benefits I get from it. I love that the Python implementation allows me to get the benefits without forcing it on me. In my distant past I very much loved coding in C, but was quite happy with Python's lack of strict typing. This feels like a good middle-ground.
If you are at work doing professional programming then typing helps avoid bugs and makes programming more of a reliable and robust process.
But doing your own thing, doing little utilities, banging out quick stuff, do exactly what makes you happy.
Programming should be fun not a chore and if types make it a chore for you then drop them.
Personally I find myself more comfortable and productive using types. Stating your types has a similar benefit to journaling, in my view. It's a forcing function for clarifying your ideas about the problem domain. Some perceive this as overhead, I perceive this as front loading. If my ideas are murky, I will run into trouble sooner or later. The later it is, the more painful it will be.
I think it largely comes down to different habits of working and thinking. I don't think one way is superior to another inherently (though types are important for collaboration), but that different people work in different ways.
We have pyre enforcement at work, the problem is, that it has been gradually turned on over time, so some stuff is pyre compliant (or just strategically ignored) and some stuff isnt, so when you open some old code to do something, you have a million errors to deal with.
That would be fine if types were enforceable. in runtime type hinting does shit all.
I would dearly love a "strict" mode where duck typing is turned off and variable are statically typed. However I suspect that will never happen, even though it'd speed up a load of stuff if done correctly (type inference happens a lot)
I suspect to use type hints properly, I'd need to think a bit more C-like and create dataclasses as types to make things more readable, rather than using Dict[str,int] or what ever.
It sounds to me like you're describing the NewType pattern which is just slightly farther down the page you linked in the article.
It's perfectly feasible to write maintainable, well-designed code in a dynamic language. I've worked with some extremely robust and ergonomic Clojure codebases before, for example. However, in Clojure, the language pushes you into its own "pit of success".
Personally, I never feel that with Python.
I just started learning the language and it’s been a ton of fun (especially babashka omg) but I’m so typescript-minded that it’s been really tough being back in dynamic land.
I miss Clojure.
EDIT: Here's a great talk Rich Hickey gave about it at LispNYC, which I was lucky enough to attend.
Code like this [0] could simply be 3 functions. Instead it's 3 classes, plus a base class `AstNode`, just so the author can appease the type checker by writing `body: List[AstNode]` instead of the dynamically-typed `body = []`.
[0] https://gist.github.com/sportsracer/16a1e294966cfba83ba61e6a...
For example, see Jack Diederich's talk "Stop Writing Classes": https://www.youtube.com/watch?v=o9pEzgHorH0
Consider FastAPI. It uses functions as endpoints, like flask. Very compatible with "Stop Writing Classes." It also leverages type hinting to eliminate boilerplate and create more concise code. You don't have to put validation or dependency injection logic at the top of every endpoint, it's handled for you so you can dedicate screen space to the problems you're solving.
Consider also the pythonism, "explicit is better than implicit." If memory serves, "Stop Writing Classes" wasn't so much about not writing containers for data but not writing containers for behavior when it wasn't associated with data. Behavior can live as a freestanding function just as well as inside of an object. But it's difficult to understand the semantics of freestanding nontrivial data, like dictionaries or long tuples.
Dataclasses and pydantic models require a minimum of boilerplate and couple the data with it's semantic meaning, so that it's preserved across boundaries. I for one am never going back to the Python before these tools.
Note that you only need `body: list[AstNode] = []` if you declare an empty container since the type checker cannot infer the type. If you're using list comprehensions or initialising the list with an element, the type checker will infer the type. You can even have a heterogeneous list with objects from multiple types, and the type checker will infer the type as the union.
In modern typed Python, you usually only annotate function signatures or stuff like dataclasses.
Don't get me wrong, I understand the benefit of the type hint - and if typing `: list[AstNode]` was all it took to get that benefit it would be a no-brainer. But that's not all it took - instead, to pass the type checker the entire script has become twice as long as it needs to be. The actual logic is hidden among multiple class definitions which are all unnecessary except to satisfy the type checker.
I agree with Armin Ronacher [0]: "types add value and they add cost". Perhaps the cost is worth paying for most programs - but there are plenty of other languages that support that way of working. For those programs where the cost might outweigh the benefits, I used to love writing Python as a more concise alternative to those languages - but instead nowadays it has become a poor attempt at mimicking them.
I'd use them if I could, but I'm trapped in Python for several reasons. If I could use Rust, Ocaml, etc., I'd do that. But my choice is between untyped and typed Python only, and I strongly believe that typed Python is much better.
Typing hints are also a moving target: they have changed, sometimes significantly, on every minor Python release since they came to be.
The `Optional` type came and went (being replaced by the new Union syntax.) Type classes are usually born in the `typing` module, then some of them get moved to `collections`, `abc`, or `collections.abc`. Some types have even been moved several times. `TypeAlias` came and went (replaced by `type`). `List` became `list`. `Tuple` became `tuple`. Forward declarations require a workaround by using string literals. But in Python 3.14, they no longer do, and the workaround will become deprecated.
I'm an evangelist for static type checking, and I never write a Python function signature without complete typing annotations. I also think that typing annotations in Python are evolving in a good way. However, for long-lived Python scripts, I highly recommend that developers are aware of, and factor in, the effort necessary to keep up with the evolution of static typing across Python versions. That effort spent on migrating is going to come on top of the mental load that typing hints already add to your plate.
Spoiler alert: the search result will be three-figure.
Some of the results are already scheduled for removal.
> The redundant types are deprecated as of Python 3.9. However, while the aliases may be removed at some point, removal of these aliases is not currently planned. As such, no deprecation warnings are currently issued by the interpreter for these aliases.
The idea is that new code shouldn't use them, but they work perfectly fine and will keep working in the near future. Even if they decide to remove these at some point, you're looking at several years before it actually happens, and you'll have plenty of time to migrate.
As it stands, you can use them to your heart's content without issues.
Your quote also conveniently left out the following paragraph:
> The aliases are guaranteed to remain in the typing module without deprecation warnings until at least Python 3.14.
Python 3.14 is less than a year away.
> Even if they decide to remove these at some point, you're looking at several years before it actually happens, and you'll have plenty of time to migrate.
I agree. I still think it adds to the pile of chores, especially for people who are on the fence anyway as to whether type annotations are worth the additional mental load to them.
For x in tests, type annotations, and documentation*:
If you write your x first then you have to decide what your API is. This is great if you want to think about your API. Sometimes though you just want to get down to it and play around with a new idea. Either way is fine.
As soon as you start sharing code or patching production code or patching someone else’s production code, one must insist on seeing some kind of x. Having x around the outside of a system — rather than requiring x be added throughout the entire system — is often good enough.
*The useful, architecture kind.
Cleaner/safer function args and return types is a common motivation for dataclass. has benefits over many/complex args besides typing too.
type R = ReturnType<typeof someFunction>
type P = Parameters<typeof someFunction>
[1] https://www.typescriptlang.org/docs/handbook/utility-types.h...[2] https://www.typescriptlang.org/docs/handbook/utility-types.h...
switching to the typed variants whether typecript, python type-hints, mypy etc will force you to do the dance to make the compiler happy instead of working on code.
which is why for me - JSDoc is really good - it uses types for documentation and ends there.
A) The type system is fairly bad, as type systems go (forgivable) B) the type checkers are, for large codebases, excruciatingly slow, to the point that the tests are faster!!
The second is not forgivable.
Hyperbolically: You have to be able to edit code at the speed of thought - whatever it takes - or else programming languages cease to be a more useful tool than just thinking.
If you type slower than you think, or can’t do the type-hint-based textual translation as quickly as you think, then… yeah - it’s not good for you.
The advice I’d wanna hear for myself is: just get better. But the advice I’d give to my coworkers is: have explicit domains where you’re able to do whatever is most efficient and effective, and then when you hand off data to the next subsystem - obey a contract. A schema. Be that type hints or a .proto file or a database schema or an API. Doesn’t matter.
1. design a memory layout for faster execution
2. press dot and get suggestions in IDE
other usage of types brings more problem than it solves.
Yes, thats the point.
In the way we resolve these issues in natural language, we can resolve bugs in python, that is, “do you mean integer ’3’ or string’3’” instead of insisting we define everything always forever.
To me, people who use type hinting are just letting me know they have written code that doesn’t check in line.
The article and the feature has nothing to do with strong types.
Strong type system limits things like 1+”1”.
Static type system requires type declarations (“int i”).
Python always had strong dynamic types.
Yes, that's the point. We use typing so the type checker can find the mistakes for us instead of adding `isinstance` everywhere.