sec05 - Chapter 4: Python Best Practice: Separating Language Management Logic with Modules (SRP)
スポンサーリンク

How to Structure a Python Package for Multilingual Support

In this chapter, we continue refining the “Fortune Cookie” application we have built so far.

At this point, fortune.py is already well-structured. However, as supported languages increase, you'll need to add or update the logic that “switches which module to load based on the language code.”

    # Switch modules based on the language code
    if lang_code == 'en':
        import message.language_en as lang
    else:  # 'jp' or else
        import message.language_jp as lang

To address this, we will extract this management logic into manager/language_manager.py. By splitting the files, we can reduce the impact of changes and make the system safer to modify.

  • When updating text or messages: translators or content editors only need to modify message/language_**.py, reducing the risk of accidentally altering core logic in fortune.py.
  • When adding a new language: simply create a new language_**.py file and register it in language_manager.py. No changes to fortune.py are required.
  • Easier testing: language-specific data and the selection logic can be unit-tested independently, making it easier to isolate issues.

In short, separating modules improves readability, safety, extensibility, and team development efficiency. This is the essence of packaging and the Single Responsibility Principle.

Final Package Structure and Module Layout in Python

Ultimately, the message/ directory will “store messages and fortune data,” while the manager/ directory will “contain logic for selection and configuration.”

fortune_cookie/
├─ fortune.py
├─ message/
│   ├─ __init__.py
│   ├─ language_en.py
│   └─ language_jp.py
└─ manager/
    ├─ __init__.py
    └─ language_manager.py

Create the following new directory and files:

  • manager/
  • __init__.py
    • This file is required for Python to recognize manager/ as a package.
  • language_manager.py
スポンサーリンク

Modularizing Language Management with language_manager

We will create a module dedicated to “selecting and managing languages.”

# manager/language_manager.py

def get_language_module(lang_code):
    '''Return the module that matches the given language code'''
    if lang_code == 'en':
        from message import language_en
        return language_en
    elif lang_code == 'jp':
        from message import language_jp
        return language_jp
    else:
        print('Unsupported language code. Using Japanese instead.')
        from message import language_jp
        return language_jp

The language_manager module provides the get_language_module() function, which dynamically loads and returns the module associated with the given language code.

Update fortune.py as follows:

# fortune.py
import random

import manager.language_manager as lang_manager


def main():
    # Choose language
    lang_code = input('Choose a language (jp/en): ')
    # Receive the language-specific module
    lang_module = lang_manager.get_language_module(lang_code)
    messages = lang_module.MESSAGES
    fortunes = lang_module.FORTUNES

    # Open a cookie?
    print(f"=== {messages['app_title']} ===")
    answer = input(messages['prompt_open'])

    if answer == 'y':
        print(f"{messages['fortune_prefix']} {random.choice(fortunes)}")
    else:
        print(messages['goodbye'])


if __name__ == '__main__':
    main()

The changes in fortune.py are as follows:

  • Line 4: import manager.language_manager as lang_manager
    • Imports the language_manager module.
  • Line 11: lang_module = lang_manager.get_language_module(lang_code)
    • Receives the language module returned by get_language_module().
  • Lines 12–13: The variables lang_module.MESSAGES and lang_module.FORTUNES are reassigned to messages and fortunes.
    • You could access them directly in print statements—for example, lang_module.MESSAGES['app_title']—but this reduces readability. Assigning them to shorter local variables improves clarity and keeps the code concise.
  • Lines 25–26: Starting in this chapter, fortune.py’s main() function is wrapped inside the if __name__ == '__main__': block.
    • This prevents main() from running automatically when fortune is imported from another module.

Summary: Benefits of Modularizing a Multilingual Python App

  • New languages can now be added by editing only language_manager.py.
  • fortune.py can focus solely on application logic.
  • get_language_module() is now testable on its own.
    • Previously, you had to run fortune.py’s main() function to verify whether language modules were imported correctly. Now, you can test language_manager.py independently.
# manager/language_manager.py

def get_language_module(lang_code):
    '''Return the module that matches the given language code'''
    if lang_code == 'en':
        from message import language_en
        return language_en
    elif lang_code == 'jp':
        from message import language_jp
        return language_jp
    else:
        print('Unsupported language code. Using Japanese instead.')
        from message import language_jp
        return language_jp


if __name__ == '__main__':
    # Temporarily modify sys.path for testing
    import sys
    sys.path.append(f'{__file__}/../../')

    from pprint import pprint

    lang_module = get_language_module('jp')
    pprint(lang_module.MESSAGES)
    pprint(lang_module.FORTUNES)
Code for testing
  • If you want to test modules individually, you can safely place test code inside the if __name__ == '__main__': block.
  • sys.path is a list that stores the search paths Python checks when importing modules. The import statements on lines 6 and 9 above, from message import language_xx, assume that fortune_cookie/ is the project root. However, if you run manager/language_manager.py directly, Python treats manager/ as the root, causing these imports to fail. To avoid errors, we add the correct root directory to sys.path.
    • __file__ is a special variable that holds the path of the file being executed. By combining this path with /../../, we move up two directories relative to language_manager.py, effectively pointing to fortune_cookie/.
      (Relative paths will be covered in detail in a dedicated lecture.)

Relative path:

f'{__file__}/../../'
# Example after replacing "__file__" with the actual path
C:\Users\xxx\Documents\python101\fortune_cookie\manager\language_manager.py/../../
Python can generally handle both backslashes \ and forward slashes / in paths. (For strict cross-platform compatibility, however, using the os module to manipulate paths is recommended.)

Absolute path:

C:\Users\xxx\Documents\python101\fortune_cookie
スポンサーリンク