Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

Sure, but now you have to rewrite your code to accept a ValidatedPosition as input and not two floats. In Rust that might be "zero cost", but in Python it certainly is not, even if you implement all this in Cython. And even in the context of a Cython/Rust/whatever extension, what if this is calling a BLAS function or some other external procedure that has specific preconditions?

It's usually fine to use the type state pattern as described by sibling commenters, but sometimes it's expensive or infeasible.

Another situation is less related to performance and more related to managing dynamic state. Consider the following annoyance that arises when we try to attach static types to an old-school Python idiom:

  class AppClient:
      def __init__(self, url: str, http_client: httpx.Client | None = None) -> None:
          self.url = url
          self.http_client = httpx.Client() if http_client is None else http_client
          self._auth_token: str | None = None

      def login(self) -> None:
          self._auth_token = ...

      def _post(self, body: dict[str, Any]) -> None:
          self.http_client.post(
              self.url, headers={"Authorizaton": f"Bearer {self._auth_token}"}, json=body
          )

      def submit_foo(self, foo: Foo) -> None:
          self._post(foo.to_dict())
Users will end up with a difficult-to-debug auth failure if they forget to call `login` first. Also, Mypy won't catch this without contorting your code.

We naturally would want to add a test case like this:

  def test_error_if_not_logged_in():
      app_client = AppClient(url="https://api.example.net/v1")
      with pytest.raises(RuntimeError):
          app_client._post({})
Thus we'll need to guard every single usage of `self._auth_token` with:

  if self._auth_token is None:
      raise RuntimeError(...)
And there are three usual approaches for handling this:

1. Disregard the article's advice and push the "if" down, into the _post method, to ensure that we only actually need to write that guard condition once.

2. Write a _check_logged_in method that encapsulates the if/raise logic, use that to at least make it easier to apply the guard where needed.

3. Refactor the internals to use something akin to the type state pattern, which is a funky and cool idea that I support. But it could easily end up turning into an "enterprise fizzbuzz" exercise where your little client library expands into a type-driven onion that obfuscates the business logic.

I'm proposing a 4th way:

4. Declare that _post may only be called from inside a specific dynamic context, and ensure that that dynamic context is only established by login().

Is it better? I'm not sure. But I've never seen it before, and it's kind of appealing as I continue to think about it.



Something I've seen a few times in Go and java: Make login() a constructor for the AppClient, or declare what's essentially a factory called "AppClientConfig" with a login() function to create the AppClient.

I've only recently started to think about using factories like that, so I'm very sure there are patterns of dynamic state I wouldn't have a recipe for, but it's a prety appealing idea to me.


That's called RAII, and I'm a big fan (not of the name, though. It's a terrible name).

https://en.m.wikipedia.org/wiki/Resource_acquisition_is_init...


That's another good way to do it for sure, and IMO badly underused within the Python ecosystem specifically.


This is just poor API design. It's that simple. `login` is on the wrong object. I'm not going to write it all out but consider this snippet:

``` def login(self) -> AppClient ```




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: