Hacker News new | ask | show | jobs
by jnwatson 16 days ago
You're holding it (Python) wrong. Python OO was a counter reaction to the bondage and discipline that languages like C++ had with private members and protected inheritance.

If you have members that users probably shouldn't touch, you prepend them with an underscore. This is just a hint; It doesn't actually change anything. We're all adults here and we know the consequences of reaching into implementation details.

6 comments

I agreed with this 100% for a long time. Then I started working on a library at $WORK with dozens of downstream users abusing the hell out of my idiomatic underscore usage, especially in the context of lazy tests with folks writing endless mocks. When I’d “break” their test suite (blocking some time sensitive release) I’d get all kinds of shit. But _they_ were breaking the contract. Unfortunately I had little (if any) control on the path of application code making it to production (yeah yeah not great engineering org, but it’s the world I lived in). Strategies like this post would be helpful for said situations.
There’s always the extra idiomatic __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED for coworkers that can’t take a hint.

https://github.com/reactjs/react.dev/issues/3896

You can technically "enforce" this at runtime with __getattribute__ or decorators[1].

Maybe be evil and add 1000 "Private access not allowed: {name}" with 1 second delays between each.

Aaaand, this flexibility is exactly why python is slow.

[1] https://pypi.org/project/accessify/

I'm torn. On the one hand, this is not too uncommon of a problem to run into. On the other, poor practices from coworkers are not going to go away thanks to a language filter.

So, the question will come down to which causes more grief, people abusing this convention, or people that overly use the language features that combat it? It is the standard optimization question between poor practices and enforcement that you have in any question of enforcement.

I would be delighted if we could get some empirical data on this.

I generally agree with you (and disagree with the parent comment) that I think this seems like a useful technique. But just note that if someone really wants to break the contract, they can just use the "private" class. Just like "private" members, it is only private by convention.
> Then I started working on a library at $WORK with dozens of downstream users abusing the hell out of my idiomatic underscore usage [...]. When I’d “break” their test suite [...] I’d get all kinds of shit

I can understand this happening once. But, after that, management should have been involved and addressed the root problem. It would be no longer a technical or communication issue, but a project management one.

My secret for test-only code is to provide functions whose first argument is a value that only exist in a test scenario. There are also static analysis rules for the linter in my language of choice that disallows test-only functions being called outside of test files. This gets you closer to "we're actually adults here" but still a tiny bit cheatable.
Something similar happened to me. I told those groups to pound sand because they knew they were relying on something which they should not. Manager had my back, they whined a lot but they had to change and improve their processes.
> We're all adults here and we know the consequences of reaching into implementation details.

I wish you were right but, IMHE, it requires a lot of communication once teams grow and many team member do not fully understand the consequences of what they do. It is nice to have something that helps when reviewing code.

> If you have members that users probably shouldn't touch, you prepend them with an underscore

Well, this is precisely what TFA does. It prepends the constructor with an underscore.

I think you missed the issue at hand:

> even if you keep all your fields private, the constructor is still, inherently, public.

ShippingOptions and the literals / enums are part of the public API, so the user would just be writing

    ShippingOptions(Carrier.USPS, Conveyance.Air)
with no hint that they're doing anything wrong.

Dataclasses do have a `kw_only` option, but I'm not sure how well underscore prefixes would be understood as private parameters / a private ctor, whereas wrapping a clearly "private" type should be clear to everybody.

Glyph is not entirely correct on the "any class" bit as you can always break the default init path:

  class ShippingOptions:
      _ship: Literal["fast", "normal", "slow"]
      __init__ = None


  def shipFast() -> ShippingOptions:
      opts = object.__new__(ShippingOptions)
      opts._ship = "fast"
      return opts
however that's a pretty ugly pattern, and unlike the one they propose I doubt tooling would understand it.
nothing is stopping adult users from disabling the type checker and using your internal type directly. the newtype is just a private class mechanism that comes with better tooling to validate that you aren't breaking the intended contract.
Are we, though? No offence but if an external module starts calling an internal function they better prepare a PR that changes the internal (yes, I know, by convention) to a public one. This is a signal to the developers of that module that they have to maintain the behavior.

That PR might well be rejected. And you have to work with the module owners to get your case supported.

Anything else is not responsible and I would not call it "adult".

or they could just test and maintain the hack on their own.
Yeah, good luck with that. I've had too many libraries change underneath me by clearly violating the previous contract that I know it doesn't work.
"Glyph" knows. He has been in the Python inner circle for decades back to when the circle promoted "spam and eggs" and "consenting adults".

Like the rest of that circle, he moves with the times, supports public shaming of Tim Peters and others and now promotes poorly implemented information hiding so Python ticks a few more boxes for the industry.

Information hiding in a language that allows changing the values of small integers at runtime via ctypes is doomed anyway. And there are plenty of better languages that do it out of the box and in a straightforward manner.

I don't know who any of these people are. This seems very gossipy.