diff --git a/CHANGELOG.md b/CHANGELOG.md
index e048783a204866246ff68e054efdadb395b847b1..e069212ae9f12b3cea5e9b5b4bd51906bf42c85d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,4 +1,5 @@
-# v2.21.4
+# v2.22.0rc1
 
 * moar tests for CPManager
-* added tainting
\ No newline at end of file
+* added tainting
+* added Environments
\ No newline at end of file
diff --git a/docs/coding/environment.rst b/docs/coding/environment.rst
new file mode 100644
index 0000000000000000000000000000000000000000..46fe521079a5f2c7e9a14ed00d1506fc9672bed6
--- /dev/null
+++ b/docs/coding/environment.rst
@@ -0,0 +1,21 @@
+Sometimes, you have a bunch of arguments to functions
+such as the database connection object, and would
+prefer them not to be passed by an argument, but configured
+through Satella's Environments themselves:
+
+.. autoclass:: satella.coding.Context
+    :members:
+
+You can think of them as a stack of cards each carrying a set of variable names. The variable, if not present
+on the current card, will be checked upwards for parent of this card. Each thread, by default, gets a
+separate hierarchy. Removing a variable on one card will not affect it's parent, however the variable will remain
+inaccessible for the duration of this context. Use them like this:
+
+.. code-block:: python
+
+    with Context() as ctxt:
+        ctxt.value = 55
+        with Context() as new_ctxt:
+            new_ctxt.value = 66
+            assert ctxt.value == 66
+        assert ctxt.value == 55
diff --git a/docs/index.rst b/docs/index.rst
index 2ffd07412087b8384ad899c24d8bcfb46e6f50f0..1baf94281037b357ac2541a3ffacb8825a5f1b72 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -13,6 +13,7 @@ Visit the project's page at GitHub_!
            configuration/schema
            configuration/sources
            coding/ctxt_managers
+           coding/environment
            coding/functions
            coding/futures
            coding/structures
diff --git a/satella/__init__.py b/satella/__init__.py
index 68faeb8bf0bc18a23e81f9b28c929371d2c42b51..9015163e78de0fadeee4903d6861cdef1a2e1f84 100644
--- a/satella/__init__.py
+++ b/satella/__init__.py
@@ -1 +1 @@
-__version__ = '2.21.4a2'
+__version__ = '2.22.0rc2'
diff --git a/satella/coding/__init__.py b/satella/coding/__init__.py
index b8963fa4d7c99dd537db82e6d03f5a7db76d3aca..612957d321d6b21dc52a7360b7a4219211612f49 100644
--- a/satella/coding/__init__.py
+++ b/satella/coding/__init__.py
@@ -17,6 +17,7 @@ from .misc import update_if_not_none, update_key_if_none, update_attr_if_none, q
     update_key_if_not_none, source_to_function, update_key_if_true, \
     get_arguments, call_with_arguments, chain_callables, Closeable, contains, \
     enum_value
+from .environment import Context
 from .overloading import overload, class_or_instancemethod
 from .recast_exceptions import rethrow_as, silence_excs, catch_exception, log_exceptions, \
     raises_exception, reraise_as
@@ -24,7 +25,7 @@ from .expect_exception import expect_exception
 from .deep_compare import assert_equal, InequalityReason, Inequal
 
 __all__ = [
-    'EmptyContextManager',
+    'EmptyContextManager', 'Context',
     'assert_equal', 'InequalityReason', 'Inequal',
     'Closeable', 'contains', 'enum_value', 'reraise_as',
     'expect_exception',
diff --git a/satella/coding/environment.py b/satella/coding/environment.py
new file mode 100644
index 0000000000000000000000000000000000000000..c0482a1d344dcc9156e5ec8f4309b61ad45cc906
--- /dev/null
+++ b/satella/coding/environment.py
@@ -0,0 +1,105 @@
+from __future__ import annotations
+import typing as tp
+import threading
+from satella.coding.typing import V
+
+from .misc import Closeable
+
+
+local = threading.local()
+
+
+THEY_HATIN = object()
+
+
+class Context:
+    """
+    New layer of environment. Can have it's own variables, or can hoist them onto the parent.
+    """
+
+    def __init__(self, parent: tp.Optional[Context] = None, **variables):
+        self.parent = parent
+        self.variables = {}
+        self.bool = None
+
+    def __str__(self):
+        return str(id(self))
+
+    def push_up(self, item: str) -> None:
+        """
+        Advance current variable to the top of the card stack.
+
+        :param item: variable name
+        """
+        var = self.variables.pop(item)
+        self.parent.variables[item] = var
+
+    def __getattr__(self, item: str):
+        if item in self.variables:
+            v = self.variables[item]
+            if v is not THEY_HATIN:
+                return v
+            raise AttributeError()
+        if self.parent is not None:
+            return getattr(self.parent, item)
+        raise AttributeError()
+
+    def __enter__(self):
+        global local
+        try:
+            parent = local.thread_context
+        except AttributeError:
+            parent = None
+        ctxt = Context(parent=parent)
+        if ctxt is not parent:
+            ctxt.parent = parent
+        local.thread_context = ctxt
+        return ctxt
+
+    def __setattr__(self, key: str, value: V):
+        """
+        Set a value
+        """
+        if key in ('parent', 'variables', 'bool'):
+            return super().__setattr__(key, value)
+        else:
+            self.variables[key] = value
+
+    def __delattr__(self, item: str) -> None:
+        self.variables[item] = THEY_HATIN
+
+    def does_exist(self, val: str) -> bool:
+        """
+        Does a given value exist on stack for this call of function?
+        """
+        if val in self.variables:
+            if self.variables[val] is THEY_HATIN:
+                return False
+            return True
+        else:
+            if self.parent is None:
+                return False
+            return self.parent.does_exist(val)
+
+    def __exit__(self, exc_type, exc_val, exc_tb) -> bool:
+        if self.parent is None:
+            try:
+                del local.thread_context
+            except AttributeError:
+                pass
+        else:
+            local.thread_context = self.parent
+        return False
+
+    @staticmethod
+    def get() -> Context:
+        """
+        Return a local context for this thread
+        """
+        global local
+        try:
+            return local.thread_context
+        except AttributeError:
+            ctxt = Context()
+            local.thread_context = ctxt
+            return ctxt
diff --git a/satella/debug/test_environment.py b/satella/debug/test_environment.py
new file mode 100644
index 0000000000000000000000000000000000000000..c6c743466e18bd7ce4d306feae5037c495b997c3
--- /dev/null
+++ b/satella/debug/test_environment.py
@@ -0,0 +1,31 @@
+import unittest
+
+from satella.coding.environment import Context
+
+
+class TestEnvs(unittest.TestCase):
+
+    def test_envs(self):
+        ctxt = Context.get()
+        ctxt.value = 5
+        self.assertEqual(ctxt.value, 5)
+        with Context() as new_ctxt:
+            self.assertEqual(new_ctxt.value, 5)
+
+    def test_delete_envs(self):
+        ctxt = Context.get()
+        ctxt.value = 5
+        self.assertEqual(ctxt.value, 5)
+        with Context() as new_ctxt:
+            del new_ctxt.value
+            self.assertRaises(AttributeError, lambda: new_ctxt.value)
+        self.assertEqual(ctxt.value, 5)
+
+    def test_nesting(self):
+        with Context() as ctxt:
+            ctxt.value = 55
+            with Context() as new_ctxt:
+                new_ctxt.value = 66
+                assert new_ctxt.value == 66
+            assert ctxt.value == 55
+