Factory / Builder Design Pattern in Python
IScreens is a callable
Actually, your IScreens class (interface) contains only one abstract method (the main method), so this class is a kind of callable.
Instead of creating a specific class, you can use collections.Callable:
import collections

IScreens = collections.Callable

To use that class, you can do:
class Screen1(IScreens):
    def __init__(self, data, config):
          ...

    def __call__(self, *args, **kwargs):  # instead of `main`
        return ...

s1 = Screen1(data=..., config=...)
result = s1()  # call the callable s1

IScreens can contain common data
I see that the data and config variables are common to Screen1 and Screen2.
It's a good practice to factorize this in the base class:
class IScreens(collections.Callable):
    def __init__(self, data, config):
        self.data = data
        self.batch = config[&quot;cfg1&quot;]


NOTE: Here, I don't understand why you pass a config dictionary, but only get the batch (config[&quot;cfg1&quot;]).

Avoid extra complexity
Private methods with double underscores, like __get_letter, are more difficult to debug,
because their names are disguised into _{className}__{attr_name}.
For instance:
class C:
    def __init__(self):
        self.__secret = &quot;foo&quot;

obj = C()
print(obj._C__secret)
# 'foo'

Very short functions like __get_letter should be avoided, unless reused several times.
Here, you add complexity (and extra computation time) with them.
Getters should not mutate the instance
A getter should not change the class instance state.
Here, in your __get_letter method, you are extracting a value (self.data[self.batch].str[:1])
and modifying the internal state (self.data[&quot;campaign&quot;]). Is it intentional?
It is better to initialize everything in your constructor:
class Screen1(IScreens):
    def __init__(self, data, config):
        self.data = data
        self.batch = config[&quot;cfg1&quot;]
        self.data[&quot;campaign&quot;] = self.data[self.batch].str[:1]
        self.data[&quot;num&quot;] = self.data[self.batch].str[1:]
        self.data[&quot;other_stuff&quot;] = self.data[&quot;num&quot;].astype(int) * 100 / 2

Factory registration
There are two different ways to implement factories:

The factory (or the main function) knows all its classes and is responsible for the class registration,
Each class knows the factory and it can auto-register itself, like a plugin.

It seems that you choose the first case. You may also need to add a method to unregister.
In the second case, registration is usually final: its more difficult to unregister…
Both are good, so.
To register a class in a factory, we used to use the class name (or a class instance) instead of an arbitrary name.
Example using the class name:
class ScreenFactory:
    def __init__(self):
        self._screens = {}

    def register_screen(self, screen_class):
        self._screens[screen_class.__name__] = screen_class

    def get_screen_class(self, class_name: str):
        return self._screens[class_name]

Example using the class attribute:
class IScreens:
    kind = &quot;unknown&quot;


class Screen1(IScreens):
    kind = &quot;s1&quot;


class ScreenFactory:
    def __init__(self):
        self._screens = {}

    def register_screen(self, screen_class):
        self._screens[screen_class.kind] = screen_class

    def get_screen_class(self, kind: str) -&gt; IScreens:
        return self._screens[kind]

The second method is interesting if you want to select an implementation (a IScreens class)
according to a tag/label extracted from textual data…

NOTE: you may have noticed that I added a get_screen_class method.

Factory access

This is the difficult part

In your get_screen method, you are checking if a specific configuration is valid (existence of &quot;screen_indicator&quot;)
and if there is a suitable class which satisfy a second condition (config[&quot;screen_indicator&quot;] is a kind of IScreens).
I am OK with that, but it can be simplified. Here is a solution with error handling:
class ScreenNotImplementedError(Exception):
    fmt = &quot;Screen not implemented for this configuration: {config}&quot;

    def __init__(self, *, config):
        msg = self.fmt.format(config=config)
        super(ScreenNotImplementedError, self).__init__(msg)


class ScreenFactory:
    ...

    def get_screen(self, data, config):
        try:
            kind = config[&quot;screen_indicator&quot;]  # may raise KeyError
            cls = self.get_screen_class(kind)  # may raise KeyError
        except KeyError:
            raise ScreenNotImplementedError(config=config)
        else:
            screen = cls(data=data, config=config)
            return screen