Skip to content

Private

Why

Object-oriented programming has a lot of advantages over procedural programming. The one of these advantages is encapsulation of data. In python encapsulation is implemented without information hiding. We lack this feature and want to fix it.

Leading underscores

The naming convention with leading underscores from pep8 does not work the way we want. For example, the code below will not hide class attributes.

>>> class User:
...     def __init__(self, name):
...         self._name = name
...     def greet(self):
...         return f"Hello, {self._name}"
...

>>> user = User("Jeff")

>>> user.greet()
'Hello, Jeff'

>>> user._name  # No information hiding here :(
'Jeff'

It may be a nice convention to have it in the code base, but

  • it does not work (it does not make architecture any better)
  • it harms readability (underscores are ugly)

Principles

All methods are public

The main purpose of objects in object-oriented programming is in behavior they could provide to the client code.

Behavior intended by object could be expressed with its methods.

Thus every instance and class method of the object is public. Class should be decorated with @private function.

>>> from generics import private

>>> @private
... class User:
...     def __init__(self, name):
...         self.name = name
...
...     def __repr__(self):
...         return f"User({self.name=!r})"
...
...     def greet(self):
...         return f'Hello, {self.name}'

>>> User
Private::User

>>> user = User('Jeff')
>>> user
Private::User(self.name='Jeff')

>>> user.greet()
'Hello, Jeff'

All attributes are private and hidden

The main advantage of objects in object-oriented programming is in encapsulation and information hiding.

Procedural programming have a lot of problems related to coupling. All data flows directly through the execution flow. Every part of the code in this flow is highly coupled with each other because of the data they pass to each other.

In opposite in the object-oriented programming we put parts of the data close to the parts of the code where it'll be used. Thus the execution flow sees only method calls. Client code does not know what data were encapsulated inside the object. And it's none of its business.

Thus all attributes encapsulated inside the objects are private. The constructor is the only place where you can put anything inside the object. Own methods of the object (defined in its class) are free to use its attributes as usual. Attribute access in the client code will raise an exception.

>>> user.name
Traceback (most recent call last):
  ...
AttributeError: 'Private::User' object has no attribute 'name'

Static methods are forbidden

Good objects expose their behavior and hide their state. The behavior objects expose should be related to the state object hides. This metric is called cohesion.

Static methods can't access the inner state of the object. That's why the behavior they expose doesn't relate to the object. Cohesion will go down. That's why we forbid static methods.

If you need such behavior, put it outside of the class. If this behavior is necessary in the instance method of the original class, encapsulate it. Pass that new thing to the constructor and access it in methods.

>>> from generics import private

>>> @private
... class User:
...     def __init__(self, name):
...         self.name = name
...
...     def greet(self):
...         return f'Hello, {self.name}'
...
...     @staticmethod
...     def is_bot():
...         return False
Traceback (most recent call last):
  ...
_generics.exceptions.GenericClassError: Do not use static methods (use composition instead)

Class methods are forbidden

As we mentioned earlier, objects should expose behavior. Class methods do not have access to any kind of inner state sinse there is no object encapsulating it. That's why we forbid class methods.

>>> from generics import private

>>> @private
... class User:
...     @classmethod
...     def create(cls):
...         pass
...
...     def __init__(self, name):
...         self.name = name
...
...     def greet(self):
...         return f'Hello, {self.name}'
Traceback (most recent call last):
  ...
_generics.exceptions.GenericClassError: Do not use class methods (call constructor instead)

Instance methods can return instances

In case your object return another instance of the same class from its method, this instance would be @private as well. This is a good practice to follow. For example, instead of assigning new value to the attributes (aka setters), create new instance of the class with necessary changes and return it from method. Immutability is a powerful technique, which would help you to build safe architecture suitable for multi-threaded applications.

See more in Prefer immutable classes.

Instance methods can not be called on classes

In some cases, it's technically possible to use instance methods as functions, if you pass value of self directly as argument. This behavior is hacky. That's why generics library forbid explicitly a call of instance methods using class attribute access.

>>> from generics import private

>>> @private
... class User:
...     def __init__(self, name):
...         self.name = name
...
...     def greet(self):
...         return 'Hello, anonymous'

>>> class AnotherUser:
...     name = 'Jeff'

>>> User.greet(AnotherUser())
Traceback (most recent call last):
  ...
_generics.exceptions.GenericClassError: Instance methods can not be called on classes

At least one instance method is required

As we mention a couple of times earlier the main goal of the good objects is to expose behavior. If there is no methods defined on the class, it's not possible to expose any kind of behavior. The object becomes useless.

Class methods does not count since it has a different purpose.

>>> from generics import private

>>> @private
... class User:
...     def __init__(self, name):
...         self.name = name
Traceback (most recent call last):
  ...
_generics.exceptions.GenericClassError: Define at least one instance method

At least one encapsulated attribute is required

The same as a previous one this rules exists because of the encapsulation restrictions. If your object does not encapsulate at least one attribute, it does not have any state. In that case behavior exposed by the object does not relate to the object itself. Thus there is no reason to define such kind of method on the class in the first place.

>>> from generics import private

>>> @private
... class User:
...
...     def greet(self):
...         return 'Hello, Jeff'
Traceback (most recent call last):
  ...
_generics.exceptions.GenericClassError: Define at least one encapsulated attribute

Note

generics library assumes that constructor of the decorated class will assign its arguments to the instance properties with same names. That's why encapsulated attributes are inferred from constructor arguments.

Variable-length encapsulated attributes are forbidden

Classes should not encapsulate average set of attributes. This makes code hard to reason about. This makes object state unrepresented in the source code.

>>> from generics import private

>>> @private
... class User:
...
...     def __init__(self, *args):
...         self.args = args
...
...     def greet(self):
...         return 'Hello, Jeff'
Traceback (most recent call last):
  ...
_generics.exceptions.GenericClassError: Class could not have variable encapsulated attribute

Keyword encapsulated attributes are forbidden

Classes should not encapsulate attributes which names defined outside of the class. This makes code hard to reason about. This makes object structure unrepresented in the source code.

>>> from generics import private

>>> @private
... class User:
...
...     def __init__(self, **kwargs):
...         self.kwargs = kwargs
...
...     def greet(self):
...         return 'Hello, Jeff'
Traceback (most recent call last):
  ...
_generics.exceptions.GenericClassError: Class could not have keyword encapsulated attribute

Implementation inheritance is forbidden

First of all, there are two types of inheritance - subtyping inheritance and implementation inheritance.

It's nothing wrong with subtyping inheritance. It's used to create "is a" relationship between classes. It's a technique where you can say that this class is an implementation of this particular interface. abc.Meta is one of the possible approaches for implicit interface implementation in Python. It has alternatives like Duck typing and typing_extensions.Protocol. If you want to design contracts in your codebase using abc.Meta, we advice you not to do so. Protocol would make it possible to define contract close to the place where object of this interface would be used. The most practical place to put interface declaration. More importantly it would not create broken (from the point of logic) import graph and would not force you to create dumb top-down architecture. abc.Meta makes both of these goals hard to achieve.

On the other hand, implementation inheritance was designed for code reuse in its heart. That's where things start to get out of hands. Implementation inheritance breaks encapsulation by its definition. That's why it's easy to end up with code like that:

>>> from app import Entity

>>> class User(Entity):
...     database_table = 'users'
...     json_fields = ['id', 'name', 'surname', 'bio']
...     permissions = ['can_read', 'can_edit']
...     query_string_params = ['user_*']

Looking just at that code it's impossible to answer these simple yet important questions:

  1. What responsibilities it has? (What it do?)
  2. How to use this class? (What public methods does it have?)

Everything is hidden from us in the base class. And it has its own base classes as well. You get where it's going.

That's why we forbid implementation inheritance.

>>> from generics import private

>>> from app import Entity

>>> @private
... class User(Entity):
...     def __init__(self, name):
...         self.name = name
...
...     def greet(self):
...         return f'Hello, {self.name}'
Traceback (most recent call last):
  ...
_generics.exceptions.GenericClassError: Do not use inheritance (use composition instead)

Note

Subtyping inheritance with abc.Meta is not implemented yet. We have plans to implement it in the future.

Underscore names are forbidden

As we mentioned at the beginning of the document, Python has a convention for private attributes and methods to start with a single underscore (_) character. This harms readability. Thus they are forbidden. The library hides everything properly anyway.

The presence of private methods especially is a sign of bad design. That means that the class has too many layers of abstractions hidden in it. The intention to hide methods defined in the class means that they operate on a lower level of abstractions that the responsibility of the class belongs to. Instead of using the composition of objects and encapsulation, the author decides to use uglier names. That gives hope to the author that users will not use methods with ugly names in their code. A bad design indeed.

>>> from generics import private

>>> @private
... class User:
...     def __init__(self, name, _surname):
...         self.name = name
...         self._surname = _surname
...
...     def greet(self):
...         return f'Hello, {self.name} {self._surname}'
Traceback (most recent call last):
  ...
_generics.exceptions.GenericClassError: Do not use private attributes

Class attributes are forbidden

If you keep in mind that classes decorated with @private decorator does not expose instance attributes, you would agree that class attributes in that case became useless.

Usually, it would be used as some kind of marker for external purposes. Let say some kind of regestry could access class attribute to make decision what to do with class.

Bad:

>>> class User:
...     bot_flag = False

>>> def register(cls):
...     if cls.bot_flag:
...         print('extend bot registry')

>>> register(User)

Good:

>>> from generics import private

>>> @private
... class User:
...     def __init__(self, name):
...         self.name = name
...
...     def is_bot(self):
...         return False

>>> def register(visitor):
...     if visitor.is_bot():
...         print('extend bot registry')

>>> register(User('Jeff'))

In some cases people tend to use class attributes as some kind of constants to be used by methods of the class. Most common intent for such style would be abbility to override that constant using inheritance. We consider this practice an antipattern as well. Our suggestion would be to move this constant to the default value of the keyword argument of the method.

If you need this constant in more than one method of your class, it is a clear sign that your class is doing too much. It's a code smell related to the Single-responsibility principle. Do not define module level uppercase-named variable. Do not repeat same keyword argument in many methods of the same class. Instead of this you could extract logic related to the constant into a smaller low-level class. After that you would be able to encapsulate its instance into original class and remove existed duplication.

Bad:

>>> class User:
...     active_status = 'active'
...
...     def __init__(self, status):
...         self.status = status
...
...     def is_active(self):
...         return self.status == self.active_status

>>> user = User('banned')
>>> user.is_active()
False

Good:

>>> from generics import private

>>> @private
... class User:
...     def __init__(self, status):
...         self.status = status
...
...     def is_active(self, active_status='active'):
...         return self.status == active_status

>>> user = User('banned')
>>> user.is_active()
False

Which that reasons in mind generics library deny class attributes to be defined on classes decorated with @private decorator.

>>> from generics import private

>>> @private
... class User:
...     alive = True
...
...     def __init__(self, name):
...         self.name = name
...
...     def greet(self):
...         return f'Hello, {self.name}'
Traceback (most recent call last):
  ...
_generics.exceptions.GenericClassError: Do not define attributes on classes

Prefer immutable classes

Awoid changing inner state of the classes as much as possible.

Way too many problems was caused because of changed data in unexpected parts of code. To deal with such problem we should respect transparency of the references. Always create new name for the new state.

Instead of protecting different parts of the code with same conditional statements, make methods of a class return a new instance of the class if it need to change something.

Bad:

>>> from generics import private

>>> @private
... class User:
...     def __init__(self, name):
...         self.name = name
...
...     def rename(self, name):
...         self.name = name

>>> User(name='Jeff').rename('John')

Good:

>>> from generics import private

>>> @private
... class User:
...     def __init__(self, name):
...         self.name = name
...
...     def __repr__(self):
...         return f"User({self.name=!r})"
...
...     def rename(self, name):
...         return User(name)

>>> User(name='Jeff').rename('John')
Private::User(self.name='John')

Methods would have representation

In some cases, instead of object composition people would create composition of callables. Usually, this happens when you pass bound method of one object into constructor of another object. Service objects tend to do this a lot. Most of the time they primary goal to trigger some action. Such objects are rarely interested in knowledge who would implement the action. To make service objects representation look nice, generics library provides representation to class and instance methods of @private classes.

>>> @private
... class Registration:
...     def __init__(self, send_message):
...         self.send_message = send_message
...
...     def __repr__(self):
...         return f"Registration(\n    {self.send_message=!r}\n)"
...
...     def sign_up(self, phone):
...         self.send_message(phone)

>>> @private
... class Message:
...     def __init__(self, text):
...         self.text = text
...
...     def __repr__(self):
...         return f"SignUp({self.text=!r})"
...
...     def send(self, phone):
...         print(f"Message {self.text!r} was sent to {phone!r}")

>>> message = Message("Welcome to the service")

>>> registration = Registration(message.send)

>>> registration.sign_up("911")
Message 'Welcome to the service' was sent to '911'

>>> registration
Private::Registration(
    self.send_message=Private::SignUp(self.text='Welcome to the service').send
)

— ⭐ —