Repository: ethereum/web3.py

Issue:
eth.sign_typed_data jsonMessage argument type discrepancy
* Version: 6.15.1
* Python: 3.11
* OS: macos
* `pip freeze` output

```
<put the output from running pip freeze here>
```


### What was wrong?

I get a mypy typing error when using `web.eth.sign_typed_data`, requiring a type ignore.

```python
self.web3.eth.sign_typed_data(
                    self.address, msg._body_  # type: ignore[arg-type]
                )
```

However when looking at the [docs](https://web3py.readthedocs.io/en/stable/web3.eth.html#web3.eth.Eth.sign_typed_data), it clearly says to pass the data as a dictionary, and that is what I have seen others do as well.

### How can it be fixed?

Either correct the type or correct the docs

---
**Note:** We prefer to use issues to track our work. If you think you've encountered a bug in web3py or 
have a feature request, you're in the right place. If you have implementation or usage questions, 
please refer to our [documentation](https://web3py.readthedocs.io/en/latest/) and/or join the conversation 
on [discord](https://discord.gg/GHryRvPB84).


Discussion:

Patch:
diff --git a/newsfragments/3304.misc.rst b/newsfragments/3304.misc.rst
new file mode 100644
index 00000000..ba5372d7
--- /dev/null
+++ b/newsfragments/3304.misc.rst
@@ -0,0 +1,1 @@
+Add better validation around ``percentile`` math utility function and simplify logic.
diff --git a/newsfragments/3308.bugfix.rst b/newsfragments/3308.bugfix.rst
new file mode 100644
index 00000000..252aedeb
--- /dev/null
+++ b/newsfragments/3308.bugfix.rst
@@ -0,0 +1,1 @@
+Fix typing for json data argument to ``eth_signTypedData``.
diff --git a/web3/_utils/math.py b/web3/_utils/math.py
index 7cc73890..2d3a8ef3 100644
--- a/web3/_utils/math.py
+++ b/web3/_utils/math.py
@@ -18,22 +18,20 @@ def percentile(
         )
     if percentile is None:
         raise ValueError(f"Expected a percentile choice, got {percentile}")
+    if percentile < 0 or percentile > 100:
+        raise ValueError("percentile must be in the range [0, 100]")
 
     sorted_values = sorted(values)
 
-    rank = len(values) * percentile / 100
-    if rank > 0:
-        index = rank - 1
-        if index < 0:
-            return sorted_values[0]
-    else:
-        index = rank
+    index = len(values) * percentile / 100 - 1
+    if index < 0:
+        return sorted_values[0]
 
-    if index % 1 == 0:
+    fractional = index % 1
+    if fractional == 0:
         return sorted_values[int(index)]
-    else:
-        fractional = index % 1
-        integer = int(index - fractional)
-        lower = sorted_values[integer]
-        higher = sorted_values[integer + 1]
-        return lower + fractional * (higher - lower)
+
+    integer = int(index - fractional)
+    lower = sorted_values[integer]
+    higher = sorted_values[integer + 1]
+    return lower + fractional * (higher - lower)
diff --git a/web3/eth/async_eth.py b/web3/eth/async_eth.py
index 7eae1673..6717b688 100644
--- a/web3/eth/async_eth.py
+++ b/web3/eth/async_eth.py
@@ -4,6 +4,7 @@ from typing import (
     Any,
     Awaitable,
     Callable,
+    Dict,
     List,
     Optional,
     Tuple,
@@ -637,14 +638,16 @@ class AsyncEth(BaseEth):
     # eth_signTypedData
 
     _sign_typed_data: Method[
-        Callable[[Union[Address, ChecksumAddress, ENS], str], Awaitable[HexStr]]
+        Callable[
+            [Union[Address, ChecksumAddress, ENS], Dict[str, Any]], Awaitable[HexStr]
+        ]
     ] = Method(
         RPC.eth_signTypedData,
         mungers=[default_root_munger],
     )
 
     async def sign_typed_data(
-        self, account: Union[Address, ChecksumAddress, ENS], data: str
+        self, account: Union[Address, ChecksumAddress, ENS], data: Dict[str, Any]
     ) -> HexStr:
         return await self._sign_typed_data(account, data)
 
diff --git a/web3/eth/eth.py b/web3/eth/eth.py
index 206a3577..4b37f965 100644
--- a/web3/eth/eth.py
+++ b/web3/eth/eth.py
@@ -2,6 +2,7 @@ from typing import (
     TYPE_CHECKING,
     Any,
     Callable,
+    Dict,
     List,
     Optional,
     Sequence,
@@ -622,7 +623,7 @@ class Eth(BaseEth):
     # eth_signTypedData
 
     sign_typed_data: Method[
-        Callable[[Union[Address, ChecksumAddress, ENS], str], HexStr]
+        Callable[[Union[Address, ChecksumAddress, ENS], Dict[str, Any]], HexStr]
     ] = Method(
         RPC.eth_signTypedData,
         mungers=[default_root_munger],
diff --git a/web3/utils/caching.py b/web3/utils/caching.py
index c315e908..caa5b372 100644
--- a/web3/utils/caching.py
+++ b/web3/utils/caching.py
@@ -16,22 +16,20 @@ class SimpleCache:
         self._data: OrderedDict[str, Any] = OrderedDict()
 
     def cache(self, key: str, value: Any) -> Tuple[Any, Dict[str, Any]]:
-        evicted_items = None
+        evicted_items = {}
         # If the key is already in the OrderedDict just update it
         # and don't evict any values. Ideally, we could still check to see
         # if there are too many items in the OrderedDict but that may rearrange
         # the order it should be unlikely that the size could grow over the limit
         if key not in self._data:
             while len(self._data) >= self._size:
-                if evicted_items is None:
-                    evicted_items = {}
                 k, v = self._data.popitem(last=False)
                 evicted_items[k] = v
         self._data[key] = value
 
         # Return the cached value along with the evicted items at the same time. No
         # need to reach back into the cache to grab the value.
-        return value, evicted_items
+        return value, evicted_items or None
 
     def get_cache_entry(self, key: str) -> Optional[Any]:
         return self._data[key] if key in self._data else None


Test patch:
diff --git a/tests/core/utilities/test_math.py b/tests/core/utilities/test_math.py
index ee677e60..a95541bd 100644
--- a/tests/core/utilities/test_math.py
+++ b/tests/core/utilities/test_math.py
@@ -36,6 +36,14 @@ def test_percentiles_with_out_of_bounds_fractions():
     assert 1 < percentile([1, 2, 3, 4], percentile=30)
 
 
+@pytest.mark.parametrize(
+    "out_of_bounds_percentile", [-2, -1, -0.1, -0.0001, 100.0001, 100.1, 101, 102, 200]
+)
+def test_percentile_out_of_bounds_values(out_of_bounds_percentile):
+    with pytest.raises(ValueError):
+        percentile(values, percentile=out_of_bounds_percentile)
+
+
 @given(
     values=st.lists(elements=st.integers(), min_size=1, max_size=200),
     p=st.integers(max_value=100, min_value=0),