Replace direct indexing with named methods.
diff --git a/mesonbuild/ast/introspection.py b/mesonbuild/ast/introspection.py
index 1d7dd05..1197510 100644
--- a/mesonbuild/ast/introspection.py
+++ b/mesonbuild/ast/introspection.py
@@ -182,7 +182,7 @@
                 if self.subproject:
                     options = {}
                     for k in comp.get_options():
-                        v = copy.copy(self.coredata.optstore[k])
+                        v = copy.copy(self.coredata.optstore.get_value_object(k))
                         k = k.evolve(subproject=self.subproject)
                         options[k] = v
                     self.coredata.add_compiler_options(options, lang, for_machine, self.environment, self.subproject)
diff --git a/mesonbuild/backend/ninjabackend.py b/mesonbuild/backend/ninjabackend.py
index d7421ff..b23eff9 100644
--- a/mesonbuild/backend/ninjabackend.py
+++ b/mesonbuild/backend/ninjabackend.py
@@ -623,7 +623,7 @@
             outfile.write('# Do not edit by hand.\n\n')
             outfile.write('ninja_required_version = 1.8.2\n\n')
 
-            num_pools = self.environment.coredata.optstore[OptionKey('backend_max_links')].value
+            num_pools = self.environment.coredata.optstore.get_value('backend_max_links')
             if num_pools > 0:
                 outfile.write(f'''pool link_pool
   depth = {num_pools}
@@ -657,7 +657,7 @@
             mlog.log_timestamp("Dist generated")
             key = OptionKey('b_coverage')
             if (key in self.environment.coredata.optstore and
-                    self.environment.coredata.optstore[key].value):
+                    self.environment.coredata.optstore.get_value(key)):
                 gcovr_exe, gcovr_version, lcov_exe, lcov_version, genhtml_exe, llvm_cov_exe = environment.find_coverage_tools(self.environment.coredata)
                 mlog.debug(f'Using {gcovr_exe} ({gcovr_version}), {lcov_exe} and {llvm_cov_exe} for code coverage')
                 if gcovr_exe or (lcov_exe and genhtml_exe):
@@ -2287,7 +2287,7 @@
         return options
 
     def generate_static_link_rules(self):
-        num_pools = self.environment.coredata.optstore[OptionKey('backend_max_links')].value
+        num_pools = self.environment.coredata.optstore.get_value('backend_max_links')
         if 'java' in self.environment.coredata.compilers.host:
             self.generate_java_link()
         for for_machine in MachineChoice:
@@ -2335,7 +2335,7 @@
             self.add_rule(NinjaRule(rule, cmdlist, args, description, **options, extra=pool))
 
     def generate_dynamic_link_rules(self):
-        num_pools = self.environment.coredata.optstore[OptionKey('backend_max_links')].value
+        num_pools = self.environment.coredata.optstore.get_value('backend_max_links')
         for for_machine in MachineChoice:
             complist = self.environment.coredata.compilers[for_machine]
             for langname, compiler in complist.items():
@@ -3727,7 +3727,7 @@
             elem.add_dep(self.generate_custom_target_clean(ctlist))
 
         if OptionKey('b_coverage') in self.environment.coredata.optstore and \
-           self.environment.coredata.optstore[OptionKey('b_coverage')].value:
+           self.environment.coredata.optstore.get_value('b_coverage'):
             self.generate_gcov_clean()
             elem.add_dep('clean-gcda')
             elem.add_dep('clean-gcno')
diff --git a/mesonbuild/build.py b/mesonbuild/build.py
index 517845c..f174ef7 100644
--- a/mesonbuild/build.py
+++ b/mesonbuild/build.py
@@ -1243,7 +1243,7 @@
         if kwargs.get(arg) is not None:
             val = T.cast('bool', kwargs[arg])
         elif k in self.environment.coredata.optstore:
-            val = self.environment.coredata.optstore[k].value
+            val = self.environment.coredata.optstore.get_value(k)
         else:
             val = False
 
diff --git a/mesonbuild/cmake/executor.py b/mesonbuild/cmake/executor.py
index a8850d6..392063d 100644
--- a/mesonbuild/cmake/executor.py
+++ b/mesonbuild/cmake/executor.py
@@ -51,7 +51,7 @@
             self.cmakebin = None
             return
 
-        self.prefix_paths = self.environment.coredata.optstore[OptionKey('cmake_prefix_path', machine=self.for_machine)].value
+        self.prefix_paths = self.environment.coredata.optstore.get_value(OptionKey('cmake_prefix_path', machine=self.for_machine))
         if self.prefix_paths:
             self.extra_cmake_args += ['-DCMAKE_PREFIX_PATH={}'.format(';'.join(self.prefix_paths))]
 
diff --git a/mesonbuild/compilers/compilers.py b/mesonbuild/compilers/compilers.py
index ef0ea70..5849d8c 100644
--- a/mesonbuild/compilers/compilers.py
+++ b/mesonbuild/compilers/compilers.py
@@ -322,9 +322,9 @@
     if option_enabled(compiler.base_options, options, OptionKey('b_bitcode')):
         args.append('-fembed-bitcode')
     try:
-        crt_val = options[OptionKey('b_vscrt')].value
-        buildtype = options[OptionKey('buildtype')].value
         try:
+            crt_val = options.get_value(OptionKey('b_vscrt'))
+            buildtype = options.get_value(OptionKey('buildtype'))
             args += compiler.get_crt_compile_args(crt_val, buildtype)
         except AttributeError:
             pass
@@ -390,9 +390,9 @@
             args.extend(linker.get_allow_undefined_link_args())
 
     try:
-        crt_val = options[OptionKey('b_vscrt')].value
-        buildtype = options[OptionKey('buildtype')].value
         try:
+            crt_val = options.get_value(OptionKey('b_vscrt'))
+            buildtype = options.get_value(OptionKey('buildtype'))
             args += linker.get_crt_link_args(crt_val, buildtype)
         except AttributeError:
             pass
diff --git a/mesonbuild/compilers/mixins/clike.py b/mesonbuild/compilers/mixins/clike.py
index 87cb819..70e81a4 100644
--- a/mesonbuild/compilers/mixins/clike.py
+++ b/mesonbuild/compilers/mixins/clike.py
@@ -26,7 +26,7 @@
 from ... import mesonlib
 from ... import mlog
 from ...linkers.linkers import GnuLikeDynamicLinkerMixin, SolarisDynamicLinker, CompCertDynamicLinker
-from ...mesonlib import LibType, OptionKey
+from ...mesonlib import LibType
 from .. import compilers
 from ..compilers import CompileCheckMode
 from .visualstudio import VisualStudioLikeCompiler
@@ -376,8 +376,8 @@
             # linking with static libraries since MSVC won't select a CRT for
             # us in that case and will error out asking us to pick one.
             try:
-                crt_val = env.coredata.optstore[OptionKey('b_vscrt')].value
-                buildtype = env.coredata.optstore[OptionKey('buildtype')].value
+                crt_val = env.coredata.optstore.get_value('b_vscrt')
+                buildtype = env.coredata.optstore.get_value('buildtype')
                 cargs += self.get_crt_compile_args(crt_val, buildtype)
             except (KeyError, AttributeError):
                 pass
diff --git a/mesonbuild/coredata.py b/mesonbuild/coredata.py
index 8804547..9f00a34 100644
--- a/mesonbuild/coredata.py
+++ b/mesonbuild/coredata.py
@@ -150,8 +150,8 @@
 
     def __calculate_subkey(self, type_: DependencyCacheType) -> T.Tuple[str, ...]:
         data: T.Dict[DependencyCacheType, T.List[str]] = {
-            DependencyCacheType.PKG_CONFIG: stringlistify(self.__builtins[self.__pkg_conf_key].value),
-            DependencyCacheType.CMAKE: stringlistify(self.__builtins[self.__cmake_key].value),
+            DependencyCacheType.PKG_CONFIG: stringlistify(self.__builtins.get_value(self.__pkg_conf_key)),
+            DependencyCacheType.CMAKE: stringlistify(self.__builtins.get_value(self.__cmake_key)),
             DependencyCacheType.OTHER: [],
         }
         assert type_ in data, 'Someone forgot to update subkey calculations for a new type'
@@ -415,27 +415,27 @@
             if opt.yielding:
                 # This option is global and not per-subproject
                 return
-            value = opts_map[key.as_root()].value
+            value = opts_map.get_value(key.as_root())
         else:
             value = None
-        opts_map[key] = opt.init_option(key, value, options.default_prefix())
+        opts_map.add_system_option(key, opt.init_option(key, value, options.default_prefix()))
 
     def init_backend_options(self, backend_name: str) -> None:
         if backend_name == 'ninja':
-            self.optstore[OptionKey('backend_max_links')] = options.UserIntegerOption(
+            self.optstore.add_system_option('backend_max_links', options.UserIntegerOption(
                 'backend_max_links',
                 'Maximum number of linker processes to run or 0 for no '
                 'limit',
-                (0, None, 0))
+                (0, None, 0)))
         elif backend_name.startswith('vs'):
-            self.optstore[OptionKey('backend_startup_project')] = options.UserStringOption(
+            self.optstore.add_system_option('backend_startup_project', options.UserStringOption(
                 'backend_startup_project',
                 'Default project to execute in Visual Studio',
-                '')
+                ''))
 
     def get_option(self, key: OptionKey) -> T.Union[T.List[str], str, int, bool]:
         try:
-            v = self.optstore[key].value
+            v = self.optstore.get_value(key)
             return v
         except KeyError:
             pass
@@ -455,11 +455,11 @@
             if key.name == 'prefix':
                 value = self.sanitize_prefix(value)
             else:
-                prefix = self.optstore[OptionKey('prefix')].value
+                prefix = self.optstore.get_value('prefix')
                 value = self.sanitize_dir_option_value(prefix, key, value)
 
         try:
-            opt = self.optstore[key]
+            opt = self.optstore.get_value_object(key)
         except KeyError:
             raise MesonException(f'Tried to set unknown builtin option {str(key)}')
 
@@ -559,8 +559,8 @@
             assert value == 'custom'
             return False
 
-        dirty |= self.optstore[OptionKey('optimization')].set_value(opt)
-        dirty |= self.optstore[OptionKey('debug')].set_value(debug)
+        dirty |= self.optstore.set_value('optimization', opt)
+        dirty |= self.optstore.set_value('debug', debug)
 
         return dirty
 
@@ -572,30 +572,32 @@
 
     def get_external_args(self, for_machine: MachineChoice, lang: str) -> T.List[str]:
         # mypy cannot analyze type of OptionKey
-        return T.cast('T.List[str]', self.optstore[OptionKey('args', machine=for_machine, lang=lang)].value)
+        key = OptionKey('args', machine=for_machine, lang=lang)
+        return T.cast('T.List[str]', self.optstore.get_value(key))
 
     def get_external_link_args(self, for_machine: MachineChoice, lang: str) -> T.List[str]:
         # mypy cannot analyze type of OptionKey
-        return T.cast('T.List[str]', self.optstore[OptionKey('link_args', machine=for_machine, lang=lang)].value)
+        key = OptionKey('link_args', machine=for_machine, lang=lang)
+        return T.cast('T.List[str]', self.optstore.get_value(key))
 
     def update_project_options(self, options: 'MutableKeyedOptionDictType', subproject: SubProject) -> None:
         for key, value in options.items():
             if not key.is_project():
                 continue
             if key not in self.optstore:
-                self.optstore[key] = value
+                self.optstore.add_project_option(key, value)
                 continue
             if key.subproject != subproject:
                 raise MesonBugException(f'Tried to set an option for subproject {key.subproject} from {subproject}!')
 
-            oldval = self.optstore[key]
+            oldval = self.optstore.get_value(key)
             if type(oldval) is not type(value):
-                self.optstore[key] = value
+                self.optstore.set_value(key, value.value)
             elif oldval.choices != value.choices:
                 # If the choices have changed, use the new value, but attempt
                 # to keep the old options. If they are not valid keep the new
                 # defaults but warn.
-                self.optstore[key] = value
+                self.optstore.set_value(key, value)
                 try:
                     value.set_value(oldval.value)
                 except MesonException:
@@ -605,7 +607,7 @@
         # Find any extranious keys for this project and remove them
         for key in self.optstore.keys() - options.keys():
             if key.is_project() and key.subproject == subproject:
-                del self.optstore[key]
+                self.optstore.remove(key)
 
     def is_cross_build(self, when_building_for: MachineChoice = MachineChoice.HOST) -> bool:
         if when_building_for == MachineChoice.BUILD:
@@ -616,13 +618,13 @@
         dirty = False
         assert not self.is_cross_build()
         for k in options.BUILTIN_OPTIONS_PER_MACHINE:
-            o = self.optstore[k]
-            dirty |= self.optstore[k.as_build()].set_value(o.value)
+            o = self.optstore.get_value_object(k)
+            dirty |= self.optstore.set_value(k.as_build(), o.value)
         for bk, bv in self.optstore.items():
             if bk.machine is MachineChoice.BUILD:
                 hk = bk.as_host()
                 try:
-                    hv = self.optstore[hk]
+                    hv = self.optstore.get_value_object(hk)
                     dirty |= bv.set_value(hv.value)
                 except KeyError:
                     continue
@@ -637,10 +639,10 @@
         pfk = OptionKey('prefix')
         if pfk in opts_to_set:
             prefix = self.sanitize_prefix(opts_to_set[pfk])
-            dirty |= self.optstore[OptionKey('prefix')].set_value(prefix)
+            dirty |= self.optstore.set_value('prefix', prefix)
             for key in options.BUILTIN_DIR_NOPREFIX_OPTIONS:
                 if key not in opts_to_set:
-                    dirty |= self.optstore[key].set_value(options.BUILTIN_OPTIONS[key].prefixed_default(key, prefix))
+                    dirty |= self.optstore.set_value(key, options.BUILTIN_OPTIONS[key].prefixed_default(key, prefix))
 
         unknown_options: T.List[OptionKey] = []
         for k, v in opts_to_set.items():
@@ -690,7 +692,7 @@
             # Always test this using the HOST machine, as many builtin options
             # are not valid for the BUILD machine, but the yielding value does
             # not differ between them even when they are valid for both.
-            if subproject and k.is_builtin() and self.optstore[k.evolve(subproject='', machine=MachineChoice.HOST)].yielding:
+            if subproject and k.is_builtin() and self.optstore.get_value_object(k.evolve(subproject='', machine=MachineChoice.HOST)).yielding:
                 continue
             # Skip base, compiler, and backend options, they are handled when
             # adding languages and setting backend.
@@ -703,14 +705,14 @@
 
         self.set_options(options, subproject=subproject, first_invocation=env.first_invocation)
 
-    def add_compiler_options(self, options: MutableKeyedOptionDictType, lang: str, for_machine: MachineChoice,
+    def add_compiler_options(self, c_options: MutableKeyedOptionDictType, lang: str, for_machine: MachineChoice,
                              env: Environment, subproject: str) -> None:
-        for k, o in options.items():
+        for k, o in c_options.items():
             value = env.options.get(k)
             if value is not None:
                 o.set_value(value)
                 if not subproject:
-                    self.optstore[k] = o  # override compiler option on reconfigure
+                    self.optstore.set_value_object(k, o)  # override compiler option on reconfigure
             self.optstore.setdefault(k, o)
 
             if subproject:
@@ -718,7 +720,7 @@
                 value = env.options.get(sk) or value
                 if value is not None:
                     o.set_value(value)
-                    self.optstore[sk] = o  # override compiler option on reconfigure
+                    self.optstore.set_value_object(sk, o)  # override compiler option on reconfigure
                 self.optstore.setdefault(sk, o)
 
     def add_lang_args(self, lang: str, comp: T.Type['Compiler'],
@@ -742,19 +744,19 @@
             else:
                 skey = key
             if skey not in self.optstore:
-                self.optstore[skey] = copy.deepcopy(compilers.base_options[key])
+                self.optstore.add_system_option(skey, copy.deepcopy(compilers.base_options[key]))
                 if skey in env.options:
-                    self.optstore[skey].set_value(env.options[skey])
+                    self.optstore.set_value(skey, env.options[skey])
                     enabled_opts.append(skey)
                 elif subproject and key in env.options:
-                    self.optstore[skey].set_value(env.options[key])
+                    self.optstore.set_value(skey, env.options[key])
                     enabled_opts.append(skey)
                 if subproject and key not in self.optstore:
-                    self.optstore[key] = copy.deepcopy(self.optstore[skey])
+                    self.optstore.add_system_option(key, copy.deepcopy(self.optstore.get_value_object(skey)))
             elif skey in env.options:
-                self.optstore[skey].set_value(env.options[skey])
+                self.optstore.set_value(skey, env.options[skey])
             elif subproject and key in env.options:
-                self.optstore[skey].set_value(env.options[key])
+                self.optstore.set_value(skey, env.options[key])
         self.emit_base_options_warnings(enabled_opts)
 
     def emit_base_options_warnings(self, enabled_opts: T.List[OptionKey]) -> None:
@@ -905,7 +907,7 @@
         if not key.is_project():
             opt = self.original_options.get(key)
             if opt is None or opt.yielding:
-                opt = self.original_options[key.as_root()]
+                opt = self.original_options.get(key.as_root())
         else:
             opt = self.original_options[key]
             if opt.yielding:
@@ -917,6 +919,9 @@
                 opt.set_value(override_value)
         return opt
 
+    def get_value(self, key):
+        return self[key].value
+
     def __iter__(self) -> T.Iterator[OptionKey]:
         return iter(self.original_options)
 
diff --git a/mesonbuild/dependencies/pkgconfig.py b/mesonbuild/dependencies/pkgconfig.py
index b6647b4..a87f413 100644
--- a/mesonbuild/dependencies/pkgconfig.py
+++ b/mesonbuild/dependencies/pkgconfig.py
@@ -238,7 +238,7 @@
     def _get_env(self, uninstalled: bool = False) -> EnvironmentVariables:
         env = EnvironmentVariables()
         key = OptionKey('pkg_config_path', machine=self.for_machine)
-        extra_paths: T.List[str] = self.env.coredata.optstore[key].value[:]
+        extra_paths: T.List[str] = self.env.coredata.optstore.get_value(key)[:]
         if uninstalled:
             uninstalled_path = Path(self.env.get_build_dir(), 'meson-uninstalled').as_posix()
             if uninstalled_path not in extra_paths:
@@ -397,7 +397,7 @@
         #
         # Only prefix_libpaths are reordered here because there should not be
         # too many system_libpaths to cause library version issues.
-        pkg_config_path: T.List[str] = self.env.coredata.optstore[OptionKey('pkg_config_path', machine=self.for_machine)].value
+        pkg_config_path: T.List[str] = self.env.coredata.optstore.get_value(OptionKey('pkg_config_path', machine=self.for_machine))
         pkg_config_path = self._convert_mingw_paths(pkg_config_path)
         prefix_libpaths = OrderedSet(sort_libpaths(list(prefix_libpaths), pkg_config_path))
         system_libpaths: OrderedSet[str] = OrderedSet()
diff --git a/mesonbuild/interpreter/interpreter.py b/mesonbuild/interpreter/interpreter.py
index a423ed8..e8547ee 100644
--- a/mesonbuild/interpreter/interpreter.py
+++ b/mesonbuild/interpreter/interpreter.py
@@ -1061,9 +1061,9 @@
                     return v
 
         try:
-            opt = self.coredata.optstore[key]
+            opt = self.coredata.optstore.get_value_object(key)
             if opt.yielding and key.subproject and key.as_root() in self.coredata.optstore:
-                popt = self.coredata.optstore[key.as_root()]
+                popt = self.coredata.optstore.get_value_object(key.as_root())
                 if type(opt) is type(popt):
                     opt = popt
                 else:
@@ -1543,7 +1543,7 @@
             if self.subproject:
                 options = {}
                 for k in comp.get_options():
-                    v = copy.copy(self.coredata.optstore[k])
+                    v = copy.copy(self.coredata.optstore.get_value_object(k))
                     k = k.evolve(subproject=self.subproject)
                     options[k] = v
                 self.coredata.add_compiler_options(options, lang, for_machine, self.environment, self.subproject)
diff --git a/mesonbuild/interpreter/interpreterobjects.py b/mesonbuild/interpreter/interpreterobjects.py
index f5dafa7..32f05ba 100644
--- a/mesonbuild/interpreter/interpreterobjects.py
+++ b/mesonbuild/interpreter/interpreterobjects.py
@@ -24,7 +24,7 @@
 from ..interpreter.type_checking import NoneType, ENV_KW, ENV_SEPARATOR_KW, PKGCONFIG_DEFINE_KW
 from ..dependencies import Dependency, ExternalLibrary, InternalDependency
 from ..programs import ExternalProgram
-from ..mesonlib import HoldableObject, OptionKey, listify, Popen_safe
+from ..mesonlib import HoldableObject, listify, Popen_safe
 
 import typing as T
 
@@ -90,7 +90,7 @@
         super().__init__(option, interpreter)
         if option and option.is_auto():
             # TODO: we need to cast here because options is not a TypedDict
-            auto = T.cast('options.UserFeatureOption', self.env.coredata.optstore[OptionKey('auto_features')])
+            auto = T.cast('options.UserFeatureOption', self.env.coredata.optstore.get_value_object('auto_features'))
             self.held_object = copy.copy(auto)
             self.held_object.name = option.name
         self.methods.update({'enabled': self.enabled_method,
diff --git a/mesonbuild/msetup.py b/mesonbuild/msetup.py
index 8f561e4..931b1eb 100644
--- a/mesonbuild/msetup.py
+++ b/mesonbuild/msetup.py
@@ -273,9 +273,9 @@
 
             # collect warnings about unsupported build configurations; must be done after full arg processing
             # by Interpreter() init, but this is most visible at the end
-            if env.coredata.optstore[mesonlib.OptionKey('backend')].value == 'xcode':
+            if env.coredata.optstore.get_value('backend') == 'xcode':
                 mlog.warning('xcode backend is currently unmaintained, patches welcome')
-            if env.coredata.optstore[mesonlib.OptionKey('layout')].value == 'flat':
+            if env.coredata.optstore.get_value('layout') == 'flat':
                 mlog.warning('-Dlayout=flat is unsupported and probably broken. It was a failed experiment at '
                              'making Windows build artifacts runnable while uninstalled, due to PATH considerations, '
                              'but was untested by CI and anyways breaks reasonable use of conflicting targets in different subdirs. '
diff --git a/mesonbuild/options.py b/mesonbuild/options.py
index 6ffcf1a..e2da6c2 100644
--- a/mesonbuild/options.py
+++ b/mesonbuild/options.py
@@ -481,13 +481,36 @@
     def __len__(self):
         return len(self.d)
 
-    def __setitem__(self, key, value):
-        self.d[key] = value
+    def ensure_key(self,key: T.Union[OptionKey, str]) -> OptionKey:
+        if isinstance(key, str):
+            return OptionKey(key)
+        return key
 
-    def __getitem__(self, key):
-        return self.d[key]
+    def get_value_object(self, key: T.Union[OptionKey, str]) -> 'UserOption[T.Any]':
+        return self.d[self.ensure_key(key)]
 
-    def __delitem__(self, key):
+    def get_value(self, key: T.Union[OptionKey, str]) -> 'T.Any':
+        return self.get_value_object(key).value
+
+    def add_system_option(self, key: T.Union[OptionKey, str], valobj: 'UserOption[T.Any'):
+        key = self.ensure_key(key)
+        self.d[key] = valobj
+
+    def add_project_option(self, key: T.Union[OptionKey, str], valobj: 'UserOption[T.Any]'):
+        key = self.ensure_key(key)
+        self.d[key] = valobj
+
+    def set_value(self, key: T.Union[OptionKey, str], new_value: 'T.Any') -> bool:
+        key  = self.ensure_key(key)
+        return self.d[key].set_value(new_value)
+
+    # FIXME, this should be removed.
+    def set_value_object(self, key: T.Union[OptionKey, str], new_object: 'UserOption[T.Any]') -> bool:
+        key  = self.ensure_key(key)
+        self.d[key] = new_object
+
+
+    def remove(self, key):
         del self.d[key]
 
     def __contains__(self, key):