Opaque Types in Python

81 points33 comments3 days ago
jnwatson

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.

show comments
gorgoiler

An alternative to consider might be to accept a Literal[“fast”, “slow”] or an Enum FAST or SLOW, and then decode that into shipping options inside the shipping code.

Only then are you truly putting a solid boundary between your library and the folks using your library. Everything else is just praying that you and only you have an underscore on your keyboard! :)

And of course another alternative is to accept that there is no true private in Python other than defdef*, so you allow your ShippingOption to be publicly visible while also documenting that the helper-constructors are what should really be used.

*”defdef” as in function definitions inside other function definitions — closures if you will, although I prefer to write mine as taking most if not all their parameters explicitly:

  def public(foo):
    def private(foo):
      …

    class Private:
      …  # less common

    …
nayuki

Java made opaque types possible from the very start by private and package-private constructors.

It's sad to see that many features regarding object-oriented programming and static typing are implemented worse in Python than Java. Various examples: __str__() vs. toString(); underscore vs. private; @staticmethod/@classmethod vs. static; generic types are so clunky in Python; types are not shown in the official Python standand library documentation; __init__() doesn't force you to call super() whereas it's mandatory in Java; @override (Python 3.12; year 2023) copying Java @Override (JDK 1.5; year 2004) very late; convention changing from duck typing (always available in Python) to structural typing (optional in Python, mandatory in Java).

show comments
CreRecombinase

Why not just use a dictionary, or why not just leave the type unannotated? If you really can't (or don't want to) say anything about the type, then don't. Python is dynamically typed!

show comments
sdeframond

Funny, I ran into the same pattern just a few months ago!

In practice, I found it difficult for coworkers to read and understand so I dropped the idea.

Another limitation I found is that it breaks down when you start using inheritance. For example:

```

class _A: pass

A = NewType("A", _A)

class _B(_A): pass

B = NewType("B", _B)

def foo(a: A) -> None: pass

b = B(_B())

foo(b) # Mypy is not happy: Argument 1 to "foo" has incompatible type "B"; expected "A"

foo(A(b)) # Mypy is OK

```

show comments
corwinxpro

The main problem with such approach is that `class _RealShipOpts:` is very ugly to write unit tests for. You need to import a private entity in tests. I would slightly change the presented approach, and move the "public" `ShippingOptions`, `shipFast`, etc., into a new module that is a public API, for my users to use something like `from my_lib.shipping.api import ShippingOptions`.

That way, I can use "normal" naming in `class RealShipOpts:...`, and be explicit that it's not really public for the end users (they should use the `.api` module instead).

tcdent

I'm sorry but if you write Python functions/methods in camel case I can't take you seriously.

show comments