Python configuration module for a web framework
toml is a third-party library, so should be separated from the standard library imports:

import os
import sys

import toml




ConfigBase.__init__ seems a little strange:

def __init__(self):
    self.basename = ''
    self.default_options = {}
    raise TypeError('Should not use ConfigBase directly')


Why bother to set the attributes if you're then going to throw an error? Also you should use NotImplementedError for abstract base classes, by convention. 



Config.load_config seems to be internal; are you expecting it to be called from anywhere other than Config.__init__? If not, rename it _load_config to indicate that it's an implementation detail. Also note that the temporary assignment to config is unnecessary:

def load_config(self):
    with open(FILEPATH, 'r') as f:
        return toml.load(f)


I'd also note that it seems weird to add a docstring for __getitem__ that just describes what that method is for, but then not document the non-standard load_config at all. Your target audience should already know what __getattr__ is for (or where to find out).



Talking of __getattr__, there's an awful lot of complexity hiding in there. Every other method you've written is nice and short, but this could do with breaking down. To describe it:


Get the default value
If the default is a dictionary, wrap it up and return it immediately
If not, but there's an appropriate value in the environment, return that
If not, but the default exists, return the default
If not, throw an error


That's how I'd structure the method:

default = self._get_default_value(item)
if isinstance(default, dict):  # or the more general collections.abc.Mapping
    return self._create_config_section(item, default)  # or maybe return ConfigSection(self._create_basename(item), default)
env = self._get_value_from_env(item)
if env is not None:
    return env
if default is not None:
    return default
raise ConfigError(self._create_error_message(item))


This reads like the description I wrote above, operates at a consistent level of abstraction (not delving into the details of e.g. how to get a value from the environment or where the default comes from) and reduces the nesting. 

You can split out other obvious helper methods, like self._create_env_var(item), along the way. Doing so might highlight to you that it's a little weird to do the same thing two different ways in the same method:


self.basename + '.' + item
'.'.join((self.basename, item))




Finally, I don't think the distinction between ConfigBase, Config and ConfigSection is really helping you. You interact with them the two child classes in much the same way; only the initialisation differs. Instead, I might be inclined to do:

class Config:

    def __init__(self, basename='', defaults=None):
        self.basename = basename  # should already be lstrip-ped
        if defaults is None:
            defaults = self._load_config()
        self.default_options = defaults

    def __getitem__(self, item):
        ...

    ...