Ask HN: What is it like working on a large dynamically-typed codebase?
I have worked on small-to-medium projects but only in strongly typed languages such as Java or TypeScript. I rely heavily on my IDE to tell me where a field is used, or to allow me to rename a method and still be confident that the callers have been updated, or add a parameter to a method and know my code won't compile until I've fixed the callers.
I can't see how this can work at scale in a large project without compile-time typesafety. I suppose 100% test coverage could provide some of the benefits, but not all.
If you've worked on a large Python or JS codebase, what was it like to work in? How did you make small refactors for example?
56 comments
[ 2.6 ms ] story [ 114 ms ] threadThere will always be opinions on benefits of vanilla js, but anything that needs to scale (team wise and communication tax) is simply easier with a static language.
* IDE refactoring is impossible because it can't infer the types, so if you do want to make large refactors it's a huge time sink
* It takes 3x longer to read and understand what the code is doing
* Requires almost twice as many unit tests for things that would normally be resolved by the compiler
* ci/cd can actually be slower since there's no incremental compilation
And pytypes doesn't really change that.
Horses for courses.
Data scientists giving me code and not telling me what Python libraries they are using or which dataframe to use has caused a week of headache, personally.
Languages that support type annotations (Python 3) help and AFAIK most good Python teams working on relatively recent code bases use them.
Pycharm seems to be great with all kinds of refactoring.
This is a P0 for me in a Python project, crazy to backlog something so useful. You can even turn it on for only errors so it's a little quieter, then gradually address < Errors over time.
Easy, you don't. All code is write only, and if something does need to change, you simply insist on a rewrite and creating an even bigger mess than what existed before.
Maybe with languages like Go we can have both now?
(About 72k LOC, not sure if that's considered "large".)
EDIT: Well, I guess I did work with some Hack code while contracting at FB, so that's definitely a large code base.
It was.... barely ok. Honestly I found that the biggest problem with no types was less specifically about "oops you passed a dict in but it was expecting a string haha runtime error" and more about:
[line 2359 of somefile.py, 20 calls deep]: def snafucate(id, f, connection):
[me]: ok connection is the same db connection we've been using. id is the UUID. What is f though? (clicks find-usages and picks the probably correct call site)
[that call site]: def snafucate_caller(f, connection):
[me]: [repeats this process until I deduce that f is at various points in the codebase at various call sites and sibling methods: a file IO object, a filename str, a custom type that represents a file path, a 2-tuple of (filename str, file IO object), and in one code path that I think it's actually unused, a dict that's never populated with anything.
And just lots of crap like this. It's some awful combination of poor rigor about naming, not consistently using the same schemes for args (do you usually take in IO objects, strings representing paths, or t.Union[IO,str]?), no testing, poor documentation, no type hinting, and of course the good ole' "this code has had 12 owners across 20 years, ranging from well-meaning new grads, to "experienced" people who are bad at coding, to ninjas who are too terribly clever for the rest of us to hope to keep up".
And yet somehow we still got stuff done in it. And one of the things I tried to do when working in it was either adding type hints once I figured them out, or if they were too arcane (like Union[Dict[Any,Any],str,IO,Tuple[str, IO]]) I would try to squash them down enough to make the type sane. As I was slowly dragged through the codebase, the types slowly started peppering in, and it got easier and easier, especially as I got promoted and had some juniors under my wing who I could ask to help do the same gradual-typing-as-you-spelunk process.
I kind of miss it tbh, one of the things I was doing right before I left was toying with proposing an official set of domain-specific type hints and type aliases that would've made the whole codebase a lot easier to follow if people were willing to join me in my quest.
How about
Than just reference it by name.The mindset is a little different in these languages and codebases IMHO - rather than a "very scientific" approach to refactoring and programming with a heavy reliance on the IDE, compiler, and analysis tools to get things just "right" on the first try, there is a more "system in the loop" approach to refactoring. Code is refactored, tests are run, failures are addressed using error output, and then full-stack or end-to-end tests are performed to validate a fix.
You're still doing most of the same thing, just at a different point in the development cycle. I think it's hard to argue that refactoring isn't "more expensive" this way. On the flip side, there's no "it compiled so I ship it" going on - an end to end understanding of the refactor and what it touches is essential.
Also, oftentimes your codebase ends up brittle enough that you need to start introducing speculative runtime / "testing in production" features - feature flags, phased rollout, in-depth error telemetry, etc. This is again expensive but at the end of the day I suppose a brittle core can lead to a more resilient system overall.
Rife with footguns.
Things broke all the time until we eventually re-added some form of static type checking.
> How did you make small refactors for example?
We'd write tests first to validate that _wrapped_ large chunks of functionality, commit those, then refactor. The key was to write these tests even if the existing code already had "unit tests" because we often found issues this way.
As much as I like typescript I've not really desired types in ruby. For small refactors you tend to be changing just one small part of a class hierarchy or interface and you're not going all over the place updating class signatures. For radical changes you're replacing an old concept with new in every location so you'd be giving it a bit more eyeball.
I'd also point out that people didn't have IDEs like that in the past even with strongly typed languages yet they still managed. I definitely use that tool now, but I see it as only an incremental improvement, you still have to know your codebase and understand the change you're making.