I quite agree. It's simply a slider isn't it, with "no time, no knowledge, pure guesswork" at one end and "infinite time, thorough analysis, perfect understanding" at the other end.
Clearly the left end is dangerous but the right end can also be, due to opportunity costs. Making a judgment on where the slider should be for each decision/change is a key skill of the craft.
I also find that my "context windows" can only hold so much. I can't understand everything all at once. So if I do very deep dives, I tend to forget (temporarily) other parts of the problem/stack/task/whatever. The solution for me is to create as much as possible as self-contained testable units. Not always possible, but something to strive for.
And I find that this skill of organising is the limit for how large/complex systems I can build before they become unmaintable. This limit has increased over time, thankfully.
Thanks for the tip, I went to investigate. I like everything about it except for the lack of third party messaging. I need to meet my friends where they are and that is on Signal or WhatsApp.
Maps is also non negotiable, having maps in your pocket is one of the true wins of a smartphone imo, giving you freedom to explore.
No problem. 3rd party messaging does seem like the biggest need being voiced by potential users right now. It does have a navigation app, by the way, but don’t know details yet.
My experience is that once people have static typing to lean on they focus much less on the things that in my view are more crucial to building clean, readable code: good, consistent naming and small chunks.
Just the visual clutter of adding type annotations can make the code flow less immediately clear and then due to broken windows syndrome people naturally care less and less about visual clarity.
So far off from what actually happens. The type annotations provide an easy scaffolding for understand what the code does in detail when reading making code flow and logic less ambiguous. Reading Python functions in isolation, you might not even know what data/structure you’re getting as input… if there’s something that muddles up immediate clarity it’s ambiguity about what data code is operating on.
I disagree strongly, based on 20 years of using Python without annotations and ~5 years of seeing people ask questions about how to do advanced things with types. And based on reading Python code, and comparing that to how I feel when reading code in any manifest-typed language.
>Reading Python functions in isolation, you might not even know what data/structure you’re getting as input
I'm concerned with what capabilities the input offers, not the name given to one particular implementation of that set of capabilities. If I have to think about it in any more detail than "`ducks` is an iterable of Ducklike" (n.b.: a code definition for an ABC need not actually exist; it would be dead code that just complicates method resolution) I'm trying to do too much in that function. If I have to care about whether the iterable is a list or a string (given that length-1 strings satisfy the ABC), I'm either trying to do the wrong thing or using the wrong language.
> if there’s something that muddles up immediate clarity it’s ambiguity about what data code is operating on.
There is no ambiguity. There is just disregard for things that don't actually matter, and designing to make sure that they indeed don't matter.
>I'm concerned with what capabilities the input offers, not the name given to one particular implementation of that set of capabilities. If I have to think about it in any more detail than "`ducks` is an iterable of Ducklike" (n.b.: a code definition for an ABC need not actually exist; it would be dead code that just complicates method resolution) I'm trying to do too much in that function. If I have to care about whether the iterable is a list or a string (given that length-1 strings satisfy the ABC), I'm either trying to do the wrong thing or using the wrong language.
You can specify exactly that and no more, using the type system:
def foo(ducks: Iterable[Ducklike]) -> None:
...
If you are typing it as list[Duck] you're doing it wrong.
I understand that. The point is that I gain no information from it, and would need more complex typing to gain information.
I keep seeing people trying to wrap their heads around various tricky covariance-vs-contravariance things (I personally can never remember which is which), or trying to make the types check for things that just seem blatantly unreasonable to me. And it takes up a lot of discussion space in my circles, because two or more people will try to figure it out together.
No, you do gain information from it: that the function takes an Iterable[Ducklike].
Moreover, now you can tell this just from the signature, rather than needing to discover it yourself by reading the function body (and maybe the bodies of the functions it calls, and so on ...). Being able to reason about a function without reading its implementation is a straightforward win.
>I already had that information. I understand my own coding style.
Good for you, but you're not the only person working on the codebase, surely.
>My function bodies are generally only a few lines, but my reasoning here is based on the choice of identifier name.
Your short functions still call other functions which call other functions which call other functions. The type will not always be obvious from looking at the current function body; often all a function does with an argument is forward it along untouched to another function. You often still need to jump through many layers of the call graph to figure out how something actually gets used.
An identifier name can't be as expressive as a type without sacrificing concision, and can't be checked mechanically. Why not be precise, why not offload some mental work onto the computer?
>Yes, it takes discipline, but it's the same kind of discipline as adding type annotations.
No, see, this is an absolutely crucial point of disagreement:
Adding type annotations is not "discipline"!
Or at least, not the same kind of discipline as remembering the types myself and running the type checker in my head. The type checker is good because it relieves me of the necessity of discipline, at least wrt to types.
Discipline consumes scarce mental effort. It doesn't scale as project complexity grows, as organizations grow, and as time passes. I would rather spend my limited mental effort on higher level things; making sure types match is rote clerical work, entirely suitable to a machine.
The language of "discipline" paints any mistake as a personal/moral failure of an individual. It's the language of a blame-culture.
> Good for you, but you're not the only person working on the codebase, surely.
I actually am. But I've also read plenty of non-annotated Python code from strangers without issue. Including the standard library, random GitHub projects I gave a PR to fix some unidiomatic expression (defense in depth by avoiding `eval` for example), etc. When the code of others is type-annotated, I often find it just as distracting as all the "# noqa: whatever" noise not designed to be read by humans.
And long functions are vastly more mentally taxing.
> often all a function does with an argument is forward it along untouched to another function. You often still need to jump through many layers of the call graph to figure out how something actually gets used.
Yes, and I find from many years of personal experience that this doesn't cause a problem. I don't need to "figure out how something actually gets used" in order to understand the code. That's the point of organizing it this way. This is also one of the core lessons of SICP as I understood it. The dynamic typing of LISP is not an accident.
> An identifier name can't be as expressive as a type without sacrificing concision
On the contrary: it is not restricted to referring to abstractions that were explicitly defined elsewhere.
> Why not be precise, why not offload some mental work onto the computer?
When I have tried to do it, I have found that the mental work increased.
> No, see, this is an absolutely crucial point of disagreement
In my experience, arguing co versus contra is a sign you are working with dubious design decisions in the first place. Mostly this is where I punch a hole in the type system and use Any. That way I can find all the bad architectural decisions in the codebase using grep.
IMO this is the source of much of the demand for type hints in Python. People don't want to write idiomatic Python, they want to write Java - but they're stuck using Python because of library availability or an existing Python codebase.
So, they write Java-style code in Python. Most of the time this means heavy use of type hints and an overuse of class hierarchies (e.g. introducing abstract classes just to satisfy the type checker) - which in my experience leads to code that's twice as long as it should be. But recently I heard more extreme advice - someone recommended "write every function as a member of a class" and "put every class in its own file".
I’d say I use type hints to write Python that looks more like Ocaml. Class hierarchies shallow to nonexistent. Abundant use of sum types. Whenever possible using Sequence, Mapping, and Set rather than list, dict, or set. (As these interfaces don’t include mutation, even if the collection itself is mutable.) Honestly if you’re heavily invested in object oriented modeling in Python, you’re doing it wrong. What a headache.
This is totally not how I used typed Python. I eschew classes almost entirely, save for immutable dataclasses. I don't use inheritance at all. Most of the code is freestanding pure functions.
I can remember in the mid-00s especially, Python gurus were really fond of saying "Python is not Java". But `unittest` was "inspired by" JUnit and `logging` looks an awful lot like my mental image of Log4J of the time.
> But recently I heard more extreme advice - someone recommended "write every function as a member of a class" and "put every class in its own file".
Not coincidentally, these are two of my least favorite parts of the standard library. Logging especially makes me grumpy, with its hidden global state and weird action at a distance. It’s far too easy to use logging wrong. And unittest just feels like every other unit testing framework from that era, which is to say, vastly overcomplicated for what it does.
Exactly my experience. I call Python a surprise-typed language. You might write a function assuming its input is a list, but then somebody passes it a string, you can iterate over it so the function returns something, but not what you would have expected, and things get deeply weird somewhere else in your codebase as a result. Surprise!
Type checking on the other hand makes duck typing awesome. All the flexibility, none of the surprises.
This is because of Python's special handling of iteration and subscripting for strings (so as to avoid having a separate character type), not because of the duck typing. In ordinary circumstances (e.g. unless you need to be careful about a base case for recursion - but that would cause a local fault and not "deep weirdness at a distance"), the result is completely intuitive (e.g. you ask it to add each element of a sequence to some other container, and it does exactly that), and I've written code that used these properties very intentionally.
If you passed a string expecting it to be treated as an atomic value rather than as a sequence (i.e. you made a mistake and want a type checker to catch it for you), there are many other things you can do to avoid creating that expectation in the first place.
Annotations can and should be checked. If I change a parameter type, other code using the function will now show errors. That won't happen with just documentation.
Unfortunately Python’s type system is unsound. It’s possible to pass all the checks and yet still have a function annotated `int` that returns a `list`.
True and irrelevant. Type annotations catch whole swathes of errors before they cause trouble and they nudge me into writing clearer code. I know they’re not watertight. Sometimes the type checker just can’t deal with a type and I have to use Any or a cast. Still better than not using them.
Do you mean that you're allowed to only use types where you want to, which means maybe the type checker can't check in cases where you haven't hinted enough, or is there some problem with the type system itself?
The type system itself is unsound. For example, this code passes `mypy --strict`, but prints `<class 'list'>` even though `bar` is annotated to return an `int`:
i : int | list[int] = 0
def foo() -> None:
global i
i = []
def bar() -> int:
if isinstance(i, int):
foo()
return i
return 0
print(type(bar()))
- Don't write unsound code? There's no way to know until you run the program and find out your `int` is actually a `list`.
- Don't assume type annotations are correct? Then what's the point of all the extra code to appease the type checker if it doesn't provide any guarantees?
You may as well argue that unit tests are pointless because you could cheat by making the implementations return just the hardcoded values from the test cases.
Agreed, but I've never found it to be especially problematic. The type checker still catches the vast majority of things you'd expect a type checker to catch.
If you want to be able to change the type of something at runtime, static analysis isn't always going to be able to have your back 100% of the time. Turns out that's a tradeoff that many are willing to make.
Yes, 100%. I believe that any new Python codebase should embrace typing as much as possible[1], and any JavaScript library should be TypeScript instead. A type system with holes like this is better than no type system.
[1] Unfortunately, many important 3rd party libraries aren't typed. I try to wrap them in type-safe modules or localise their use, but if your codebase is deeply dependent on them, this isn't always feasible.
It doesn't. There are cases where the type-checker can't know the type (e.g. json.load has to return Any), but there are tools in the language to reduce how much that happens. If you commit to a fully strictly-typed codebase, it doesn't happen often.
Yeah, people from statically typed languages sometimes can't understand how dynamically typed languages can even work. How can I do anything if I don't know what type to pass?! Because we write functions like "factorial(number)" instead of "int fac(int n)".
It's kind of stupidity farming isn't it. Is there anything wrong with that? The stupid people get to do some engaging, which is after all what their brain evolved to enjoy, everyone's a winner? Maybe?
And decision fatigue is a real thing. Even if the ice cream flavour/engineering decision is maybe not perfectly optimal, there's some value in not having to make the decision myself
For me doing the dishes is a 10-15 minute chore in the sink with some hot water but most people don't seem to object that their dishwasher takes 3 hours to do the same job with superheated steam or whatever. It still saves them the 15 minutes.
Don't forget that the dishwasher also uses less water and the dishes get sterilized by the steam.These features may or may not be important for a particular user.
Clearly the left end is dangerous but the right end can also be, due to opportunity costs. Making a judgment on where the slider should be for each decision/change is a key skill of the craft.