<#3492 perf: :zap: Speed up method `PropertiesList...
# meltano-repo-activity
g
#3492 perf: :zap: Speed up method `PropertiesList.items` by 43% Pull request opened by codeflash-ai[bot] on <!date^1771369059^{date_short}|2026-02-17T22:57:39Z> 📄 43% (0.43x) speedup for
PropertiesList.items
in
singersdk/typing.py
⏱️ Runtime :
69.7 microseconds
→
48.6 microseconds
(best of
250
runs) 📝 Explanation and details This optimization achieves a 43% runtime improvement (from 69.7μs to 48.6μs) by introducing a cache for the
ItemsView
object returned by
PropertiesList.items()
. Key optimization: The original implementation created a new
ItemsView
object on every call to
items()
by directly returning
self.wrapped.items()
. The optimized version caches this view in
self._items_cache
and reuses it across calls, eliminating the overhead of repeatedly constructing the view object. Why this improves runtime: 1. Reduces object allocation overhead: Creating an
ItemsView
involves Python's internal machinery for building view objects. By caching it after the first call, subsequent calls skip this allocation entirely. 2. Eliminates repeated method calls: The original version calls
.items()
on every access (1037 hits in profiling). The cache reduces this to just 34 first-time calls, with 1003 cache hits simply returning the stored reference. How this benefits existing workloads: The test suite demonstrates this optimization is particularly effective for: • Repeated iterations: The test
test_iterating_items_repeatedly_is_deterministic_and_performant
shows a 54.4% speedup when calling
items()
1000 times in a loop—exactly the pattern this cache targets. • Large property lists: Tests with 100-1000 properties still benefit from the cache when
items()
is called multiple times. • Schema introspection: Any code that queries
items()
repeatedly (e.g., validation, serialization, or UI generation) will see reduced overhead. Trade-offs: The cache is safe because
ItemsView
is a live view over the underlying
self.wrapped
dict—it automatically reflects mutations without needing invalidation. The only cost is one additional attribute (
_items_cache
) and minor slowdown on first access (208ns vs 166ns), but subsequent calls become nearly free (cache lookup vs dict method call). This is an excellent trade-off when
items()
is called multiple times per
PropertiesList
instance, which is common in schema processing pipelines. ✅ Correctness verification report: | Test | Status | | ----------------------------- | --------------- | | ⚙️ Existing Unit Tests | ✅ 5 Passed | | 🌀 Generated Regression Tests | ✅ 1070 Passed | | ⏪ Replay Tests | 🔘 None Found | | 🔎 Concolic Coverage Tests | 🔘 None Found | | 📊 Tests Coverage | 100.0% | ⚙️ Click to see Existing Unit Tests 🌀 Click to see Generated Regression Tests from future import annotations from singer_sdk.typing import ( # import the real classes under test PropertiesList, Property, ) def test_items_returns_items_view_and_preserves_order_and_identity(): # Create two Property instances with distinct names (use built-in types for the schema arg). p1 = Property("alpha", int) p2 = Property("beta", int) # Construct a PropertiesList using the real Property instances. pl = PropertiesList(p1, p2) # Retrieve the items view from the PropertiesList. codeflash_output = pl.items() items_view = codeflash_output # 166ns -> 208ns (20.2% slower) # The returned object should be a dict-items view type (same type as {}.items()). codeflash_output = type({}.items()) # 42ns -> 42ns (0.000% faster) def test_items_on_empty_propertieslist_is_empty(): # Construct an empty PropertiesList (no properties passed). pl = PropertiesList() # items() should be empty. codeflash_output = pl.items() items = codeflash_output # 125ns -> 208ns (39.9% slower) def test_items_reflects_mutations_of_underlying_wrapped_dict(): # Start with one property. p1 = Property("one", int) pl = PropertiesList(p1) codeflash_output = pl.items() items_view = codeflash_output # 166ns -> 208ns (20.2% slower) # Add a new Property directly into the wrapped dict (real attribute on the instance). p2 = Property("two", int) pl.wrapped["two"] = p2 # Remove the original property and ensure the view reflects the deletion. del pl.wrapped["one"] def test_duplicate_property_names_last_one_wins(): # Two Property instances with the same name should result in the last one overriding the first. first = Property("dup", int) second = Property("dup", float) pl = PropertiesList(first, second) items_list = list(pl.items()) # 166ns -> 208ns (20.2% slower) def test_property_names_with_special_characters_and_numbers(): # Use a variety of ASCII-safe edge-like names (dash, underscore, numeric prefix). p_dash = Property("name-with-dash", int) p_under = Property("name_with_underscore", int) p_num = Property("123numeric", int) pl = PropertiesList(p_dash, p_under, p_num) # Ensure keys are preserved exactly as provided. keys = [k for k, _ in pl.items()] def test_items_with_many_properties_preserves_count_and_order(): # Create 1000 Property instances with unique names. count = 1000 props = [Property(f"p{i}", int) for i in range(count)] # Construct a PropertiesList using the large list via argument unpacking. pl = PropertiesList(*props) items_list = list(pl.items()) # 166ns -> 208ns (20.2% slower) def test_iterating_items_repeatedly_is_deterministic_and_performant(): # Build a moderate-size PropertiesList for repeated iteration. size = 200 # 200 keeps the test fast but still exercises iteration cost pl = PropertiesList(*(Property(f"k{i}", int) for i in range(size))) # Sum up the lengths of the items view across 1000 iterations to ensure deterministic behavior. iterations = 1000 total = 0 for _ in range(iterations): # Each iteration should be quick and return the same size. total += len(pl.items()) # 64.6μs -> 41.8μs (54.4% faster) def test_iter_returns_only_property_values_and_matches_items_values(): # Verify the iter implementation returns the same Property values as the items view. pA = Property("A", int) pB = Property("B", int) pl = PropertiesList(pA, pB) # iter yields Property values; compare against the values from items(). iter_values = list(pl) # uses PropertiesList.__iter__() items_values = [v for _, v in pl.items()] # codeflash_output is used to check that the output of the original code is the same as that of the optimized code. from future import annotations from singer_sdk.typing import ( BooleanType, DecimalType, IntegerType, PropertiesList, Property, StringType, ) def test_items_returns_items_view(): """Test that items() returns an ItemsView object.""" # Create a PropertiesList with one property prop_list = PropertiesList(Property("name", StringType)) # Call items() and verify it returns an ItemsView codeflash_output = prop_list.items() result = codeflash_output # 166ns -> 208ns (20.2% slower) def test_items_empty_properties_list(): """Test items() on an empty PropertiesList.""" # Create a PropertiesList with no properties prop_list = PropertiesList() # Call items() and verify it returns an empty view codeflash_output = prop_list.items() result = codeflash_output # 166ns -> 208ns (20.2% slower) # Convert to list to check contents items_list = list(result) def test_items_single_property(): """Test items() with a single property.""" # Create a PropertiesList with one property prop = Property("username", StringType, required=True) prop_list = PropertiesList(prop) # Call items() and check the result codeflash_output = p… meltano/sdk