TL;DR Python does not have private scope, but the conventions used to “make” something private can confuse new programmers.

A common feature of many programming languages, particularly languages students are likely to encounter in university courses, is the idea of “private” scope in classes. This refers to the fact that a class can have internal state that is only visible to an instance of that class. In C++ this looks like:

class Foo
{
  public:
    Foo();
    ~Foo();
    void bar();

  private:
    void buf();
    int baz;
};

Understanding this is pretty straightforward. The methods Foo(), ~Foo(), and bar() are public, callable by any other part of the program. buf() and baz are not accesible to parts of the program that are not the object itself.

Python, however, does not have scope restrictions like this. The same class as in the above example would look like this:

class Foo:
    def __init__(self):
        self.baz = 0
    
    def bar():
        pass

    def buf():
        pass

No access modifiers, everything is public! However, a common convention in Python is prefixing private methods or attributes with an underscore, which would look like this:

class Foo:
    def __init__(self):
        self._baz = 0
    
    def bar():
        pass

    def _buf():
        pass

By convention, _foo means that the foo attribute is an internal method or field, and should not be used by programmers. However, some resources describe this as “making” something private, which is incorrect! Observe:

In [1]: class Foo: 
   ...:     def __init__(self): 
   ...:         self._baz = 0 
   ...:      
   ...:     def bar(): 
   ...:         pass 
   ...:  
   ...:     def _buf(): 
   ...:         pass 
   ...:                                                                                                               

In [2]: foo = Foo()                                                                                                   

In [3]: foo._baz                                                                                                      
Out[3]: 0

In [4]:  

One other thing I have seen in resources for new developers (looking through them for my class) is that a double underscore makes something “super private”. This is… not true. But it looks true:

In [4]: class Foo: 
   ...:     def __init__(self): 
   ...:         self.__baz = 0 
   ...:      
   ...:     def bar(): 
   ...:         pass 
   ...:  
   ...:     def _buf(): 
   ...:         pass 
   ...:                                                                                                               

In [5]: foo = Foo()                                                                                                   

In [6]: foo.__baz                                                                                                     
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-6-906e6460fe73> in <module>
----> 1 foo.__baz

AttributeError: 'Foo' object has no attribute '__baz'

In [7]:  

What happened? Turning to the vars method, we see this output:

In [7]: vars(foo)                                                                                                     
Out[7]: {'_Foo__baz': 0}

In [8]:  

So the attribute is there, just… renamed? Mangled, almost? In fact, what’s happening is called name mangling. The main idea of name manglins is that a subclass of Foo might implement an attribute also called _baz, and then you’re in trouble for deciding which one should be used. What the double underscore is actually doing is exactly what it appears to be doing: rewriting the name of the method so that it won’t conflict with a subclass. You can still access the supposedly private parts of a class with no problems at all, if you happen to know the name mangling scheme:

In [8]: foo._Foo__baz                                                                                                 
Out[8]: 0

In [9]:  

So while Python doesn’t have functionally private scope, it has conventionally private scope using _, and subclass-private scope using __.