1

I am having trouble dealing with classes wherein I have classes whose attributes and methods I want to access in my API code, but not have those attributes and methods exposed to the user who is using the API.

Consider this code example:

class Context:
    pass


class Foo(Generic[T]):
    _context: Context


class Bar(Generic[T]):
    _context: Context


def func_1(foo: Foo[str]) -> int:
    # do something with _context
    
    return NotImplemented

def func_2(foo: Foo[int], bar: Bar[int]) -> Bar[str]:
    # do something with _context
    
    return NotImplemented

The two functions are like operators that act on these objects, and it doesn't make sense to have them as methods to Foo and Bar in my application. Additionally they only act on certain types of the Foo and Bar classes. If I access foo._context inside func_1 for example, mypy and pylance will yell at me due to accessing a private attribute. But I don't want to make it public since I don't want the users of the API to access this function. How can I overcome this while still keeping my code up to typing standards?

I understand that a simple way would be to simply access foo._context inside the functions and include a typing ignore comment. However, this doesn't seem clean to me and I was wondering if there was a Pythonic way to do this. Along the same lines as this question, I was wondering if there was a way to prevent the users of an API from instantiating a public class, but still instantiate somehow within the API. I mean that these class attributes and methods should be public inside the package for developers but not for library users.

4
  • 2
    I don't think mypy ever complains about underscored attribute access (mypy Playground). Adding a # type: ignore therefore shouldn't suppress anything. Commented Oct 14, 2024 at 5:09
  • Attribute hiding is, in general, not "Pythonic". Just put in comments saying "please leave this alone". Commented Oct 14, 2024 at 5:18
  • @dROOOze pylance does show errors however. Commented Oct 14, 2024 at 10:34
  • 1
    _context means "implementation-specific" more than it means "private". Developers are by definition working on the implementation, so they are free to access it, while ordinary users are not. Commented Oct 14, 2024 at 15:08

3 Answers 3

1

I'll preface this by saying that in real life I'd probably go with one of the two options you've already rejected, namely:

  1. Make these "operators" public methods of the classes (whose implementations will be allowed to access "private" attributes without any linter complaints). Your reasoning for not wanting to do this isn't clear to me; operators are usually implemented as methods of the classes that they operate on.
  2. Just go ahead and access the private method in the implementation of your module function and add a # pylint: disable (or whatever) where needed. It's not philosophically any different from declaring a friend class in a language that has a more strict public/private concept; the ability to declare exceptions to a general rule exists for a reason.

There's also a good chance that in real life I'd be rethinking whether I wanted context to be a class attribute, or whether there should be a single object called "context" (which often can imply a sort of "god object").

That said, another option is to make the classes themselves "private" to the module, and expose a public interface to them via a Protocol or similar.

from typing import Protocol


class Context:
    pass


class Foo(Protocol[T]):
    pass  # declare "public" attributes here


class _Foo(Foo[T]):
    context: Context


class Bar(Protocol[T]):
    pass  # declare "public" attributes here


class _Bar(Bar[T]):
    context: Context


def func_1(foo: Foo[str]) -> int:
    assert isinstance(foo, _Foo)
    # do something with _Foo.context
    
    raise NotImplemented

def func_2(foo: Foo[int], bar: Bar[int]) -> Bar[str]:
    assert isinstance(bar, _Bar)
    assert isinstance(foo, _Foo)
    # do something with _Foo.context/_Bar.context
    
    raise NotImplemented
Sign up to request clarification or add additional context in comments.

1 Comment

Very helpful suggestions, especially the protocol one. I'll probably do a combination of these 3 options and choose depending on the scenario. Thanks!
0

Python (unfortunately, IMO) does not have intricate levels of access modifiers like some other languages. The natively-supported mechanism by type-checkers to do something similar is through the use of .pyi stub files.

First, you'd get rid of the leading underscores of the _context member in the .py's classes, to restore their "public"ness. Then, you'd maintain a .pyi stub which looks like this (note no context member in the classes):

# code_example.pyi

class Foo(Generic[T]):
    ...  # Other class members that you'd want to make public

class Bar(Generic[T]):
    ...  # Other class members that you'd want to make public

# "I was wondering if there was a way to prevent the users of an API
#  from instantiating a public class, but still instantiate somehow
#  within the API."

class __DONT_USE_ME_ANYWHERE: ...

class Baz:
    def __init__(self, unavailable_argument: __DONT_USE_ME_ANYWHERE) -> None:
        """
        Instantiating this class is an error outside the API. Your
        implementation of `Baz.__init__` in the corresponding `.py` file 
        has a proper (and maybe vastly different) signature.
        """

def func_1(foo: Foo[str]) -> int: ...
def func_2(foo: Foo[int], bar: Bar[int]) -> Bar[str]: ...

Then, depending on what "public inside API" means, you'd do one of the following (see the full reference for distributing type information):

  • If "public inside API" means "public inside each module, but not across modules", then you'd lay out your package like this:
    example-project/
      example_package/
        __init__.py
        code_example.py
        code_example.pyi
        py.typed
    
  • If "public inside API" means "public inside the package for developers, but not for library users", then you'd lay out your packages like this, and ensure that users of your library also install the stubs package:
    example-project/
      example_package/
        __init__.py
        code_example.py
      example_package-stubs/  # This could be in another project instead
        __init__.pyi
        code_example.pyi
    

It's a bit of maintenance to keep the stubs in sync with the implementation, but there's ways of cutting the work required, e.g.

  • Utilise ast writers to automatically generate the stubs - a minimum implementation could just be wiping function bodies and replacing them with ellipsis;
  • Only generating a partial stubs package (adding the line partial\n in py.typed).

2 Comments

This was helpful! I mean public inside the package for developers but not for library users. Are you saying that this type of intricate access modifiers are not possible in Python at all (apart from the stubs)? And would you consider using a private naming scheme but breaking that inside the API for developers (with a type ignore comment) to be easier/more Pythonic than maintaining stubs?
@person17381 In Python, the most common interpretation of an underscore naming scheme (and thus enforced by linters) is like protected or private access modifiers in languages like C++/C#/Java. This is not the same as your request in the question, which is like pub(crate) in Rust, so I would consider the idea of utilising underscores as moot (it doesn't fulfil the request). Rather than focusing on what's "more Pythonic" (some people think the whole static typing ecosystem is "not Pythonic"), I would focus on your actual request and using available Python tooling to achieve this.
0

You can define setters and getters in classes that let you work with private attributes. An example could be:

class Foo(Generic[T]):
    self._context: Context

    @property
    def context(self):
        return self._context

    @context.setter
    def context(self, text):
        self._context = text

And then you should be able to do something like foo.context within functions, where foo is an instance of Foo class.

1 Comment

Thanks for your suggestion! But this won't work since as I said I don't want the user to be able to access the context at all. It should only be used within the API code.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.