Search code examples
pythonpyinstallerexecutablecx-freezesys

Why use getattr instead of hasattr for sys.frozen?


Every documentation and answer I could find, says that in order to check if the program is "frozen" (an exe for example), we can use getattr(sys, 'frozen', False) in the following way:

import sys
if getattr(sys, 'frozen', False):
    print('program is frozen exe')
else:
    print('program is a .py script')

Where False is returned by default if the frozen attribute doesn't exist instead of throwing an AttributeError. An example from the console:

>>> getattr(sys, 'frozen')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: module 'sys' has no attribute 'frozen'
>>> getattr(sys, 'frozen', False)
False
>>> hasattr(sys, 'frozen')
False

This is all fine, but there is a shorter version of this that does the same job, unless I'm missing something:

hasattr(sys, 'frozen')

Which simply returns True or False without the need to specify a default. Despite this being shorter and possibly more readable, every documentation and answer online uses getattr instead. I'm sure there's a clever difference I might be overlooking, which is why I'm asking this question.

Example sources that refer to getattr:


Solution

  • The PyInstaller documentation itself uses the getattr style block of code here, so by the copy-paste effect, it will proliferate.

    So then the question becomes: why does the PyInstaller documentation do it this way? As others said in comments, sys.frozen could theoretically be set to False or some other falsy value, in which case, hasattr(sys, 'frozen') would still return True:

    >>> class Stuff:
    ...     pass
    ... 
    >>> x = Stuff()
    >>> x.yes = True
    >>> hasattr(x, 'yes')
    True
    >>> getattr(x, 'yes', False)
    True
    >>> x.no = False
    >>> hasattr(x, 'no')
    True
    >>> getattr(x, 'no', False)
    False
    

    But as used by PyInstaller, is it possible for sys.frozen to be set, but with a falsy value? Let's check the source code:

    $ git clone git://github.com/pyinstaller/pyinstaller
    Cloning into 'pyinstaller'...
    ...
    $ cd pyinstaller
    $ git grep '\(sys\.frozen\|frozen = \)' PyInstaller
    PyInstaller/loader/pyiboot01_bootstrap.py:    sys.frozen = True
    PyInstaller/utils/win32/winutils.py:            # True if "sys.frozen" is currently set.
    PyInstaller/utils/win32/winutils.py:            is_sys_frozen = hasattr(sys, 'frozen')
    PyInstaller/utils/win32/winutils.py:            # Current value of "sys.frozen" if any.
    PyInstaller/utils/win32/winutils.py:            sys_frozen = getattr(sys, 'frozen', None)
    PyInstaller/utils/win32/winutils.py:            sys.frozen = '|_|GLYH@CK'
    PyInstaller/utils/win32/winutils.py:            # If "sys.frozen" was previously set, restore its prior value.
    PyInstaller/utils/win32/winutils.py:                sys.frozen = sys_frozen
    PyInstaller/utils/win32/winutils.py:                del sys.frozen
    

    So this variable is only set in two scenarios:

    1. To True, while loading/bootstrapping; and
    2. To '|_|GLYH@CK' temporarily in a utility method.

    It never receives a falsy value from PyInstaller.

    So in practice, using hasattr(sys, 'frozen') will work fine. It just feels wrong from a correctness perspective, because it's an incorrectly handled edge case, which is probably why the PyInstaller documentation uses getattr instead: it's more future-proof.