Repository: ethereum/web3.py
Pull request #3643: feat: build logger messages lazily
Author: login: BobTheBuidler
Created: 2025-03-13T04:04:40Z | Merged: 2025-05-16T00:13:29Z

### What was wrong?
When using the logging module, we currently build the log message eagerly which leads to wasted cpu if the logger is not enabled. 

According to DuckDuckGo's silly AI assistant:

"Using f-strings for logging in Python is generally discouraged because they evaluate the string immediately, which can lead to unnecessary computations even if the log level is not set to display that message. Instead, it's recommended to use the traditional formatting methods, like the % operator, which defer the string formatting until it's actually needed, improving performance and avoiding potential issues."

With this change, we will build the log messages lazily so the string is never built if the log will not actually be emitted, saving compute. This is just a microoptimization but will make a difference for power users such as myself who have use cases that require making a high volume of requests over a long period.

Related to Issue # N/A
Closes # N/A

### How was it fixed?
replacing f-strings with strings which will be used with str.format

### Todo:

- [x] Clean up commit history
- [x] Add entry to the [release notes](https://github.com/ethereum/web3.py/blob/main/newsfragments/README.md)  Not applicable for typical user so I skipped this addition

#### Cute Animal Picture

![Put a link to a cute animal picture inside the parenthesis-->](<https://www.rd.com/wp-content/uploads/2021/04/GettyImages-1145794687.jpg>)



Diff:
diff --git a/docs/filters.rst b/docs/filters.rst
index 1fa44c93f4..1ccebc98a8 100644
--- a/docs/filters.rst
+++ b/docs/filters.rst
@@ -654,7 +654,7 @@ The script can be run with: ``python ./eventscanner.py <your JSON-RPC API URL>``
                     # from our in-memory cache
                     block_when = get_block_when(block_number)
 
-                    logger.debug(f"Processing event {evt['event']}, block: {evt['blockNumber']} count: {evt['blockNumber']}")
+                    logger.debug("Processing event %s, block: %s count: %s", evt['event'], block_number, block_number)
                     processed = self.state.process_event(block_when, evt)
                     all_processed.append(processed)
 
@@ -726,7 +726,8 @@ The script can be run with: ``python ./eventscanner.py <your JSON-RPC API URL>``
                 # Print some diagnostics to logs to try to fiddle with real world JSON-RPC API performance
                 estimated_end_block = min(current_block + chunk_size, end_block)
                 logger.debug(
-                    f"Scanning token transfers for blocks: {current_block} - {estimated_end_block}, chunk size {chunk_size}, last chunk scan took {last_scan_duration}, last logs found {last_logs_found}"
+                    "Scanning token transfers for blocks: %s - %s, chunk size %s, last chunk scan took %s, last logs found %s",
+                    current_block, estimated_end_block, chunk_size, last_scan_duration, last_logs_found,
                 )
 
                 start = time.time()
@@ -778,7 +779,9 @@ The script can be run with: ``python ./eventscanner.py <your JSON-RPC API URL>``
                 if i < retries - 1:
                     # Give some more verbose info than the default middleware
                     logger.warning(
-                        f"Retrying events for block range {start_block} - {end_block} ({end_block-start_block}) failed with {e} , retrying in {delay} seconds")
+                        "Retrying events for block range %s - %s (%s) failed with %s , retrying in %s seconds",
+                        start_block, end_block, end_block-start_block, e, delay,
+                    )
                     # Decrease the `eth_getBlocks` range
                     end_block = start_block + ((end_block - start_block) // 2)
                     # Let the JSON-RPC to recover e.g. from restart
@@ -832,7 +835,7 @@ The script can be run with: ``python ./eventscanner.py <your JSON-RPC API URL>``
             to_block=to_block
         )
 
-        logger.debug(f"Querying eth_getLogs with the following parameters: {event_filter_params}")
+        logger.debug("Querying eth_getLogs with the following parameters: %s", event_filter_params)
 
         # Call JSON-RPC API on your Ethereum node.
         # get_logs() returns raw AttributedDict entries
diff --git a/newsfragments/3643.performance.rst b/newsfragments/3643.performance.rst
new file mode 100644
index 0000000000..ab26a3c4fc
--- /dev/null
+++ b/newsfragments/3643.performance.rst
@@ -0,0 +1 @@
+optimize message formatting for logging
diff --git a/web3/_utils/caching/request_caching_validation.py b/web3/_utils/caching/request_caching_validation.py
index 8ebd791611..8bc3f9f4f9 100644
--- a/web3/_utils/caching/request_caching_validation.py
+++ b/web3/_utils/caching/request_caching_validation.py
@@ -73,7 +73,8 @@ def is_beyond_validation_threshold(
         else:
             provider.logger.error(
                 "Invalid request_cache_validation_threshold value. This should not "
-                f"have happened. Request not cached.\n    threshold: {threshold}"
+                "have happened. Request not cached.\n    threshold: %s",
+                threshold,
             )
             return False
     except Exception as e:
@@ -128,7 +129,8 @@ def validate_from_blocknum_in_result(
         else:
             provider.logger.error(
                 "Could not find block number in result. This should not have happened. "
-                f"Request not cached.\n    result: {result}",
+                "Request not cached.\n    result: %s",
+                result,
             )
             return False
     except Exception as e:
@@ -198,7 +200,8 @@ async def async_is_beyond_validation_threshold(
         else:
             provider.logger.error(
                 "Invalid request_cache_validation_threshold value. This should not "
-                f"have happened. Request not cached.\n    threshold: {threshold}"
+                "have happened. Request not cached.\n    threshold: %s",
+                threshold,
             )
             return False
     except Exception as e:
@@ -253,7 +256,8 @@ async def async_validate_from_blocknum_in_result(
         else:
             provider.logger.error(
                 "Could not find block number in result. This should not have happened. "
-                f"Request not cached.\n    result: {result}",
+                "Request not cached.\n    result: %s",
+                result,
             )
             return False
     except Exception as e:
diff --git a/web3/_utils/http_session_manager.py b/web3/_utils/http_session_manager.py
index f3dad3b262..32d0769bf9 100644
--- a/web3/_utils/http_session_manager.py
+++ b/web3/_utils/http_session_manager.py
@@ -81,14 +81,14 @@ def cache_and_return_session(
 
         with self._lock:
             cached_session, evicted_items = self.session_cache.cache(cache_key, session)
-            self.logger.debug(f"Session cached: {endpoint_uri}, {cached_session}")
+            self.logger.debug("Session cached: %s, %s", endpoint_uri, cached_session)
 
         if evicted_items is not None:
             evicted_sessions = evicted_items.values()
             for evicted_session in evicted_sessions:
                 self.logger.debug(
-                    "Session cache full. Session evicted from cache: "
-                    f"{evicted_session}",
+                    "Session cache full. Session evicted from cache: %s",
+                    evicted_session,
                 )
             threading.Timer(
                 # If `request_timeout` is `None`, don't wait forever for the closing
@@ -167,7 +167,7 @@ def _handle_streaming_response(
     def _close_evicted_sessions(self, evicted_sessions: List[requests.Session]) -> None:
         for evicted_session in evicted_sessions:
             evicted_session.close()
-            self.logger.debug(f"Closed evicted session: {evicted_session}")
+            self.logger.debug("Closed evicted session: %s", evicted_session)
 
     # -- async -- #
 
@@ -195,7 +195,7 @@ async def async_cache_and_return_session(
                     cache_key, session
                 )
                 self.logger.debug(
-                    f"Async session cached: {endpoint_uri}, {cached_session}"
+                    "Async session cached: %s, %s", endpoint_uri, cached_session
                 )
 
             else:
@@ -215,8 +215,10 @@ async def async_cache_and_return_session(
                 )
                 if warning:
                     self.logger.debug(
-                        f"{warning}: {endpoint_uri}, {cached_session}. "
-                        f"Creating and caching a new async session for uri."
+                        "%s: %s, %s. Creating and caching a new async session for uri.",
+                        warning,
+                        endpoint_uri,
+                        cached_session,
                     )
 
                     self.session_cache._data.pop(cache_key)
@@ -224,7 +226,8 @@ async def async_cache_and_return_session(
                         # if loop was closed but not the session, close the session
                         await cached_session.close()
                     self.logger.debug(
-                        f"Async session closed and evicted from cache: {cached_session}"
+                        "Async session closed and evicted from cache: %s",
+                        cached_session,
                     )
 
                     # replace stale session with a new session at the cache key
@@ -238,7 +241,7 @@ async def async_cache_and_return_session(
                         cache_key, _session
                     )
                     self.logger.debug(
-                        f"Async session cached: {endpoint_uri}, {cached_session}"
+                        "Async session cached: %s, %s", endpoint_uri, cached_session
                     )
 
         if evicted_items is not None:
@@ -248,8 +251,8 @@ async def async_cache_and_return_session(
             evicted_sessions = list(evicted_items.values())
             for evicted_session in evicted_sessions:
                 self.logger.debug(
-                    "Async session cache full. Session evicted from cache: "
-                    f"{evicted_session}",
+                    "Async session cache full. Session evicted from cache: %s",
+                    evicted_session,
                 )
             # Kick off an asyncio `Task` to close the evicted sessions. In the case
             # that the cache filled very quickly and some sessions have been evicted
@@ -323,10 +326,10 @@ async def _async_close_evicted_sessions(
 
         for evicted_session in evicted_sessions:
             await evicted_session.close()
-            self.logger.debug(f"Closed evicted async session: {evicted_session}")
+            self.logger.debug("Closed evicted async session: %s", evicted_session)
 
         if any(not evicted_session.closed for evicted_session in evicted_sessions):
             self.logger.warning(
-                "Some evicted async sessions were not properly closed: "
-                f"{evicted_sessions}"
+                "Some evicted async sessions were not properly closed: %s",
+                evicted_sessions,
             )
diff --git a/web3/_utils/validation.py b/web3/_utils/validation.py
index 12ed0f7d38..b911c1259c 100644
--- a/web3/_utils/validation.py
+++ b/web3/_utils/validation.py
@@ -396,7 +396,7 @@ def validate_rpc_response_and_raise_if_error(
 
         response = apply_error_formatters(error_formatters, response)
         if logger is not None:
-            logger.debug(f"RPC error response: {response}")
+            logger.debug("RPC error response: %s", response)
 
         raise web3_rpc_error
 
diff --git a/web3/manager.py b/web3/manager.py
index 0ee3e60669..5f52edc63b 100644
--- a/web3/manager.py
+++ b/web3/manager.py
@@ -159,7 +159,7 @@ def _make_request(
         request_func = provider.request_func(
             cast("Web3", self.w3), cast("MiddlewareOnion", self.middleware_onion)
         )
-        self.logger.debug(f"Making request. Method: {method}")
+        self.logger.debug("Making request. Method: %s", method)
         return request_func(method, params)
 
     async def _coro_make_request(
@@ -169,7 +169,7 @@ async def _coro_make_request(
         request_func = await provider.request_func(
             cast("AsyncWeb3", self.w3), cast("MiddlewareOnion", self.middleware_onion)
         )
-        self.logger.debug(f"Making request. Method: {method}")
+        self.logger.debug("Making request. Method: %s", method)
         return await request_func(method, params)
 
     #
@@ -366,9 +366,12 @@ async def socket_request(
     ) -> RPCResponse:
         provider = cast(PersistentConnectionProvider, self._provider)
         self.logger.debug(
-            "Making request to open socket connection and waiting for response: "
-            f"{provider.get_endpoint_uri_or_ipc_path()},\n    method: {method},\n"
-            f"    params: {params}"
+            "Making request to open socket connection and waiting for response: %s,\n"
+            "    method: %s,\n"
+            "    params: %s",
+            provider.get_endpoint_uri_or_ipc_path(),
+            method,
+            params,
         )
         rpc_request = await self.send(method, params)
         provider._request_processor.cache_request_information(
@@ -388,9 +391,12 @@ async def send(self, method: RPCEndpoint, params: Any) -> RPCRequest:
             middleware_onion,
         )
         self.logger.debug(
-            "Sending request to open socket connection: "
-            f"{provider.get_endpoint_uri_or_ipc_path()},\n    method: {method},\n"
-            f"    params: {params}"
+            "Sending request to open socket connection: %s,\n"
+            "    method: %s,\n"
+            "    params: %s",
+            provider.get_endpoint_uri_or_ipc_path(),
+            method,
+            params,
         )
         return await send_func(method, params)
 
@@ -404,7 +410,8 @@ async def recv_for_request(self, rpc_request: RPCRequest) -> RPCResponse:
         )
         self.logger.debug(
             "Getting response for request from open socket connection:\n"
-            f"    request: {rpc_request}"
+            "    request: %s",
+            rpc_request,
         )
         response = await recv_func(rpc_request)
         try:
@@ -417,8 +424,8 @@ async def recv_for_request(self, rpc_request: RPCRequest) -> RPCResponse:
     async def recv(self) -> Union[RPCResponse, FormattedEthSubscriptionResponse]:
         provider = cast(PersistentConnectionProvider, self._provider)
         self.logger.debug(
-            "Getting next response from open socket connection: "
-            f"{provider.get_endpoint_uri_or_ipc_path()}"
+            "Getting next response from open socket connection: %s",
+            provider.get_endpoint_uri_or_ipc_path(),
         )
         # pop from the queue since the listener task is responsible for reading
         # directly from the socket
@@ -501,9 +508,11 @@ async def _process_response(
                     # subscription as it comes in
                     request_info.subscription_id = subscription_id
                     provider.logger.debug(
-                        "Caching eth_subscription info:\n    "
-                        f"cache_key={cache_key},\n    "
-                        f"request_info={request_info.__dict__}"
+                        "Caching eth_subscription info:\n"
+                        "    cache_key=%s,\n"
+                        "    request_info=%s",
+                        cache_key,
+                        request_info.__dict__,
                     )
                     self._request_processor._request_information_cache.cache(
                         cache_key, request_info
diff --git a/web3/providers/ipc.py b/web3/providers/ipc.py
index 23a133843a..b4e18860da 100644
--- a/web3/providers/ipc.py
+++ b/web3/providers/ipc.py
@@ -197,7 +197,7 @@ def _make_request(self, request: bytes) -> RPCResponse:
     @handle_request_caching
     def make_request(self, method: RPCEndpoint, params: Any) -> RPCResponse:
         self.logger.debug(
-            f"Making request IPC. Path: {self.ipc_path}, Method: {method}"
+            "Making request IPC. Path: %s, Method: %s", self.ipc_path, method
         )
         request = self.encode_rpc_request(method, params)
         return self._make_request(request)
@@ -206,7 +206,7 @@ def make_request(self, method: RPCEndpoint, params: Any) -> RPCResponse:
     def make_batch_request(
         self, requests: List[Tuple[RPCEndpoint, Any]]
     ) -> List[RPCResponse]:
-        self.logger.debug(f"Making batch request IPC. Path: {self.ipc_path}")
+        self.logger.debug("Making batch request IPC. Path: %s", self.ipc_path)
         request_data = self.encode_batch_rpc_request(requests)
         response = cast(List[RPCResponse], self._make_request(request_data))
         return sort_batch_response_by_response_ids(response)
diff --git a/web3/providers/legacy_websocket.py b/web3/providers/legacy_websocket.py
index 1f978e865f..09ff5d9e38 100644
--- a/web3/providers/legacy_websocket.py
+++ b/web3/providers/legacy_websocket.py
@@ -136,7 +136,7 @@ async def coro_make_request(self, request_data: bytes) -> RPCResponse:
     @handle_request_caching
     def make_request(self, method: RPCEndpoint, params: Any) -> RPCResponse:
         self.logger.debug(
-            f"Making request WebSocket. URI: {self.endpoint_uri}, " f"Method: {method}"
+            "Making request WebSocket. URI: %s, Method: %s", self.endpoint_uri, method
         )
         request_data = self.encode_rpc_request(method, params)
         future = asyncio.run_coroutine_threadsafe(
@@ -149,8 +149,9 @@ def make_batch_request(
         self, requests: List[Tuple[RPCEndpoint, Any]]
     ) -> List[RPCResponse]:
         self.logger.debug(
-            f"Making batch request WebSocket. URI: {self.endpoint_uri}, "
-            f"Methods: {requests}"
+            "Making batch request WebSocket. URI: %s, Methods: %s",
+            self.endpoint_uri,
+            requests,
         )
         request_data = self.encode_batch_rpc_request(requests)
         future = asyncio.run_coroutine_threadsafe(
diff --git a/web3/providers/persistent/persistent.py b/web3/providers/persistent/persistent.py
index 4f70af6deb..b9ee912a75 100644
--- a/web3/providers/persistent/persistent.py
+++ b/web3/providers/persistent/persistent.py
@@ -167,6 +167,7 @@ def get_endpoint_uri_or_ipc_path(self) -> str:
             )
 
     async def connect(self) -> None:
+        endpoint = self.get_endpoint_uri_or_ipc_path()
         _connection_attempts = 0
         _backoff_rate_change = 1.75
         _backoff_time = 1.75
@@ -174,9 +175,7 @@ async def connect(self) -> None:
         while _connection_attempts != self._max_connection_retries:
             try:
                 _connection_attempts += 1
-                self.logger.info(
-                    f"Connecting to: {self.get_endpoint_uri_or_ipc_path()}"
-                )
+                self.logger.info("Connecting to: %s", endpoint)
                 await self._provider_specific_connect()
                 self._message_listener_task = asyncio.create_task(
                     self._message_listener()
@@ -184,19 +183,18 @@ async def connect(self) -> None:
                 self._message_listener_task.add_done_callback(
                     self._message_listener_callback
                 )
-                self.logger.info(
-                    f"Successfully connected to: {self.get_endpoint_uri_or_ipc_path()}"
-                )
+                self.logger.info("Successfully connected to: %s", endpoint)
                 break
             except (WebSocketException, OSError) as e:
                 if _connection_attempts == self._max_connection_retries:
                     raise ProviderConnectionError(
-                        f"Could not connect to: {self.get_endpoint_uri_or_ipc_path()}. "
+                        f"Could not connect to: {endpoint}. "
                         f"Retries exceeded max of {self._max_connection_retries}."
                     ) from e
                 self.logger.info(
-                    f"Could not connect to: {self.get_endpoint_uri_or_ipc_path()}. "
-                    f"Retrying in {round(_backoff_time, 1)} seconds.",
+                    "Could not connect to: %s. Retrying in %s seconds.",
+                    endpoint,
+                    round(_backoff_time, 1),
                     exc_info=True,
                 )
                 await asyncio.sleep(_backoff_time)
@@ -217,7 +215,8 @@ async def disconnect(self) -> None:
         await self._provider_specific_disconnect()
         self._request_processor.clear_caches()
         self.logger.info(
-            f"Successfully disconnected from: {self.get_endpoint_uri_or_ipc_path()}"
+            "Successfully disconnected from: %s",
+            self.get_endpoint_uri_or_ipc_path(),
         )
 
     @async_handle_send_caching
@@ -337,9 +336,9 @@ def _raise_stray_errors_from_cache(self) -> None:
 
     async def _message_listener(self) -> None:
         self.logger.info(
-            f"{self.__class__.__qualname__} listener background task started. Storing "
-            "all messages in appropriate request processor queues / caches to be "
-            "processed."
+            "%s listener background task started. Storing all messages in "
+            "appropriate request processor queues / caches to be processed.",
+            self.__class__.__qualname__,
         )
         while True:
             # the use of sleep(0) seems to be the most efficient way to yield control
@@ -363,8 +362,8 @@ async def _message_listener(self) -> None:
                 self._raise_stray_errors_from_cache()
             except PersistentConnectionClosedOK as e:
                 self.logger.info(
-                    "Message listener background task has ended gracefully: "
-                    f"{e.user_message}"
+                    "Message listener background task has ended gracefully: %s",
+                    e.user_message,
                 )
                 # trigger a return to end the listener task and initiate the callback fn
                 return
@@ -382,8 +381,9 @@ def _error_log_listener_task_exception(self, e: Exception) -> None:
         """
         self.logger.error(
             "Exception caught in listener, error logging and keeping "
-            "listener background task alive."
-            f"\n    error={e.__class__.__name__}: {e}"
+            "listener background task alive.\n    error=%s: %s",
+            e.__class__.__name__,
+            e,
         )
 
     def _handle_listener_task_exceptions(self) -> None:
@@ -416,7 +416,8 @@ async def _match_response_id_to_request_id() -> RPCResponse:
 
                 if request_cache_key in self._request_processor._request_response_cache:
                     self.logger.debug(
-                        f"Popping response for id {request_id} from cache."
+                        "Popping response for id %s from cache.",
+                        request_id,
                     )
                     popped_response = await self._request_processor.pop_raw_response(
                         cache_key=request_cache_key,
diff --git a/web3/providers/persistent/request_processor.py b/web3/providers/persistent/request_processor.py
index 6d74711523..47a957d818 100644
--- a/web3/providers/persistent/request_processor.py
+++ b/web3/providers/persistent/request_processor.py
@@ -128,7 +128,9 @@ def cache_request_information(
             if cache_key in self._request_information_cache:
                 self._provider.logger.debug(
                     "This is a cached request, not caching request info because it is "
-                    f"not unique:\n    method={method},\n    params={params}"
+                    "not unique:\n    method=%s,\n    params=%s",
+                    method,
+                    params,
                 )
                 return None
 
@@ -148,8 +150,11 @@ def cache_request_information(
             response_formatters,
         )
         self._provider.logger.debug(
-            f"Caching request info:\n    request_id={request_id},\n"
-            f"    cache_key={cache_key},\n    request_info={request_info.__dict__}"
+            "Caching request info:\n    request_id=%s,\n"
+            "    cache_key=%s,\n    request_info=%s",
+            request_id,
+            cache_key,
+            request_info.__dict__,
         )
         self._request_information_cache.cache(
             cache_key,
@@ -170,7 +175,9 @@ def pop_cached_request_information(
         if request_info is not None:
             self._provider.logger.debug(
                 "Request info popped from cache:\n"
-                f"    cache_key={cache_key},\n    request_info={request_info.__dict__}"
+                "    cache_key=%s,\n    request_info=%s",
+                cache_key,
+                request_info.__dict__,
             )
         return request_info
 
@@ -247,8 +254,9 @@ def append_result_formatter_for_request(
             )
         else:
             self._provider.logger.debug(
-                f"No cached request info for response id `{request_id}`. Cannot "
-                f"append response formatter for response."
+                "No cached request info for response id `%s`. Cannot "
+                "append response formatter for response.",
+                request_id,
             )
 
     def append_middleware_response_processor(
@@ -269,13 +277,16 @@ def append_middleware_response_processor(
                 )
             else:
                 self._provider.logger.debug(
-                    f"No cached request info for response id `{response_id}`. Cannot "
-                    f"append middleware response processor for response: {response}"
+                    "No cached request info for response id `%s`. Cannot "
+                    "append middleware response processor for response: %s",
+                    response_id,
+                    response,
                 )
         else:
             self._provider.logger.debug(
                 "No response `id` in response. Cannot append middleware response "
-                f"processor for response: {response}"
+                "processor for response: %s",
+                response,
             )
 
     # raw response cache
@@ -302,7 +313,7 @@ async def cache_raw_response(
                 await self._provider._listen_event.wait()
 
             self._provider.logger.debug(
-                f"Caching subscription response:\n    response={raw_response}"
+                "Caching subscription response:\n    response=%s", raw_response
             )
             subscription_id = raw_response.get("params", {}).get("subscription")
             sub_container = self._subscription_container
@@ -320,16 +331,20 @@ async def cache_raw_response(
             # constant cache key for the batch response.
             cache_key = generate_cache_key(BATCH_REQUEST_ID)
             self._provider.logger.debug(
-                f"Caching batch response:\n    cache_key={cache_key},\n"
-                f"    response={raw_response}"
+                "Caching batch response:\n    cache_key=%s,\n    response=%s",
+                cache_key,
+                raw_response,
             )
             self._request_response_cache.cache(cache_key, raw_response)
         else:
             response_id = raw_response.get("id")
             cache_key = generate_cache_key(response_id)
             self._provider.logger.debug(
-                f"Caching response:\n    response_id={response_id},\n"
-                f"    cache_key={cache_key},\n    response={raw_response}"
+                "Caching response:\n    response_id=%s,\n"
+                "    cache_key=%s,\n    response=%s",
+                response_id,
+                cache_key,
+                raw_response,
             )
             self._request_response_cache.cache(cache_key, raw_response)
 
@@ -354,13 +369,15 @@ async def pop_raw_response(
                 if self._subscription_queue_synced_with_ws_stream:
                     self._subscription_queue_synced_with_ws_stream = False
                 self._provider.logger.info(
-                    f"Subscription response queue has {qsize} subscriptions. "
-                    "Processing as FIFO."
+                    "Subscription response queue has %s subscriptions. "
+                    "Processing as FIFO.",
+                    qsize,
                 )
 
             self._provider.logger.debug(
                 "Subscription response popped from queue to be processed:\n"
-                f"    raw_response={raw_response}"
+                "    raw_response=%s",
+                raw_response,
             )
         else:
             if not cache_key:
@@ -372,8 +389,9 @@ async def pop_raw_response(
             if raw_response is not None:
                 self._provider.logger.debug(
                     "Cached response popped from cache to be processed:\n"
-                    f"    cache_key={cache_key},\n"
-                    f"    raw_response={raw_response}"
+                    "    cache_key=%s,\n    raw_response=%s",
+                    cache_key,
+                    raw_response,
                 )
 
         return raw_response
diff --git a/web3/providers/persistent/subscription_manager.py b/web3/providers/persistent/subscription_manager.py
index c5879f22e1..4b8e4e8121 100644
--- a/web3/providers/persistent/subscription_manager.py
+++ b/web3/providers/persistent/subscription_manager.py
@@ -123,8 +123,9 @@ async def subscribe(
             subscriptions._id = sub_id
             self._add_subscription(subscriptions)
             self.logger.info(
-                "Successfully subscribed to subscription:\n    "
-                f"label: {subscriptions.label}\n    id: {sub_id}"
+                "Successfully subscribed to subscription:\n    label: %s\n    id: %s",
+                subscriptions.label,
+                sub_id,
             )
             return sub_id
         elif isinstance(subscriptions, Sequence):
@@ -190,8 +191,10 @@ async def unsubscribe(
             if await self._w3.eth._unsubscribe(subscriptions.id):
                 self._remove_subscription(subscriptions)
                 self.logger.info(
-                    "Successfully unsubscribed from subscription:\n    "
-                    f"label: {subscriptions.label}\n    id: {subscriptions.id}"
+                    "Successfully unsubscribed from subscription:\n"
+                    "    label: %s\n    id: %s",
+                    subscriptions.label,
+                    subscriptions.id,
                 )
 
                 if len(self._subscription_container.handler_subscriptions) == 0:
@@ -208,7 +211,7 @@ async def unsubscribe(
             unsubscribed: List[bool] = []
             # re-create the subscription list to prevent modifying the original list
             # in case ``subscription_manager.subscriptions`` was passed in directly
-            subs = [sub for sub in subscriptions]
+            subs = list(subscriptions)
             for sub in subs:
                 if isinstance(sub, str):
                     sub = HexStr(sub)
@@ -216,7 +219,8 @@ async def unsubscribe(
             return all(unsubscribed)
 
         self.logger.warning(
-            f"Failed to unsubscribe from subscription\n    subscription={subscriptions}"
+            "Failed to unsubscribe from subscription\n    subscription=%s",
+            subscriptions,
         )
         return False
 
@@ -240,7 +244,8 @@ async def unsubscribe_all(self) -> bool:
             if len(self.subscriptions) > 0:
                 self.logger.warning(
                     "Failed to unsubscribe from all subscriptions. Some subscriptions "
-                    f"are still active.\n    subscriptions={self.subscriptions}"
+                    "are still active.\n    subscriptions=%s",
+                    self.subscriptions,
                 )
             return False
 
diff --git a/web3/providers/rpc/async_rpc.py b/web3/providers/rpc/async_rpc.py
index 6e66bae938..8a227977d5 100644
--- a/web3/providers/rpc/async_rpc.py
+++ b/web3/providers/rpc/async_rpc.py
@@ -156,14 +156,16 @@ async def _make_request(self, method: RPCEndpoint, request_data: bytes) -> bytes
     @async_handle_request_caching
     async def make_request(self, method: RPCEndpoint, params: Any) -> RPCResponse:
         self.logger.debug(
-            f"Making request HTTP. URI: {self.endpoint_uri}, Method: {method}"
+            "Making request HTTP. URI: %s, Method: %s", self.endpoint_uri, method
         )
         request_data = self.encode_rpc_request(method, params)
         raw_response = await self._make_request(method, request_data)
         response = self.decode_rpc_response(raw_response)
         self.logger.debug(
-            f"Getting response HTTP. URI: {self.endpoint_uri}, "
-            f"Method: {method}, Response: {response}"
+            "Getting response HTTP. URI: %s, Method: %s, Response: %s",
+            self.endpoint_uri,
+            method,
+            response,
         )
         return response
 
@@ -171,7 +173,7 @@ async def make_request(self, method: RPCEndpoint, params: Any) -> RPCResponse:
     async def make_batch_request(
         self, batch_requests: List[Tuple[RPCEndpoint, Any]]
     ) -> Union[List[RPCResponse], RPCResponse]:
-        self.logger.debug(f"Making batch request HTTP - uri: `{self.endpoint_uri}`")
+        self.logger.debug("Making batch request HTTP - uri: `%s`", self.endpoint_uri)
         request_data = self.encode_batch_rpc_request(batch_requests)
         raw_response = await self._request_session_manager.async_make_post_request(
             self.endpoint_uri, request_data, **self.get_request_kwargs()
@@ -191,4 +193,4 @@ async def disconnect(self) -> None:
             await session.close()
         cache.clear()
 
-        self.logger.info(f"Successfully disconnected from: {self.endpoint_uri}")
+        self.logger.info("Successfully disconnected from: %s", self.endpoint_uri)
diff --git a/web3/providers/rpc/rpc.py b/web3/providers/rpc/rpc.py
index d60ce8c14c..24e0c35df4 100644
--- a/web3/providers/rpc/rpc.py
+++ b/web3/providers/rpc/rpc.py
@@ -164,14 +164,16 @@ def _make_request(self, method: RPCEndpoint, request_data: bytes) -> bytes:
     @handle_request_caching
     def make_request(self, method: RPCEndpoint, params: Any) -> RPCResponse:
         self.logger.debug(
-            f"Making request HTTP. URI: {self.endpoint_uri}, Method: {method}"
+            "Making request HTTP. URI: %s, Method: %s", self.endpoint_uri, method
         )
         request_data = self.encode_rpc_request(method, params)
         raw_response = self._make_request(method, request_data)
         response = self.decode_rpc_response(raw_response)
         self.logger.debug(
-            f"Getting response HTTP. URI: {self.endpoint_uri}, "
-            f"Method: {method}, Response: {response}"
+            "Getting response HTTP. URI: %s, Method: %s, Response: %s",
+            self.endpoint_uri,
+            method,
+            response,
         )
         return response
 
@@ -179,7 +181,7 @@ def make_request(self, method: RPCEndpoint, params: Any) -> RPCResponse:
     def make_batch_request(
         self, batch_requests: List[Tuple[RPCEndpoint, Any]]
     ) -> Union[List[RPCResponse], RPCResponse]:
-        self.logger.debug(f"Making batch request HTTP, uri: `{self.endpoint_uri}`")
+        self.logger.debug("Making batch request HTTP, uri: `%s`", self.endpoint_uri)
         request_data = self.encode_batch_rpc_request(batch_requests)
         raw_response = self._request_session_manager.make_post_request(
             self.endpoint_uri, request_data, **self.get_request_kwargs()
diff --git a/web3/utils/subscriptions.py b/web3/utils/subscriptions.py
index d62d0b31d8..4e1c62c3f5 100644
--- a/web3/utils/subscriptions.py
+++ b/web3/utils/subscriptions.py
@@ -87,10 +87,13 @@ async def wrapped_handler(
         sub.handler_call_count += 1
         sub.manager.total_handler_calls += 1
         sub.manager.logger.debug(
-            f"Subscription handler called.\n"
-            f"    label: {sub.label}\n"
-            f"    call count: {sub.handler_call_count}\n"
-            f"    total handler calls: {sub.manager.total_handler_calls}"
+            "Subscription handler called.\n"
+            "    label: %s\n"
+            "    call count: %s\n"
+            "    total handler calls: %s",
+            sub.label,
+            sub.handler_call_count,
+            sub.manager.total_handler_calls,
         )
         await handler(context)
 


Reviews:
user: login: kclowes
body: Awesome, thank you! I left one nit, but I think that's it. Do you want to add a newsfragment or should I?
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-05-15T20:40:47Z
user: login: fselmo
body: This lgtm too. Thanks for benchmarking!
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-05-15T21:02:11Z

Review comments:
diff_hunk: @@ -164,36 +164,34 @@ def get_endpoint_uri_or_ipc_path(self) -> str:
             )
 
     async def connect(self) -> None:
+        endpoint = self.get_endpoint_uri_or_ipc_path()
path: web3/providers/persistent/persistent.py
user: login: kclowes
body: 👍 nice!
created_at: 2025-05-15T20:27:46Z
author_association: COLLABORATOR
diff_hunk: @@ -214,7 +212,8 @@ async def disconnect(self) -> None:
         await self._provider_specific_disconnect()
         self._request_processor.clear_caches()
         self.logger.info(
-            f"Successfully disconnected from: {self.get_endpoint_uri_or_ipc_path()}"
+            "Successfully disconnected from: %s",
+            self.get_endpoint_uri_or_ipc_path(),
path: web3/providers/persistent/persistent.py
user: login: kclowes
body: ```suggestion
            endpoint,
```
created_at: 2025-05-15T20:28:30Z
author_association: COLLABORATOR

PR comments:
user: login: kclowes
created_at: 2025-03-13T22:04:06Z
body: Thank you! Before merging, I'd like to make sure that this actually makes a 'meaningful' difference in performance before we sacrifice code consistency. Do you happen to know what magnitude of faster this is? If not, would you mind running a couple benchmarks? 
author_association: COLLABORATOR
user: login: BobTheBuidler
created_at: 2025-04-05T16:54:04Z
body: This is on the backburner for now. I can tell you it saves about 33% of the time vs the existing code but I know you're going to want to see some data for this. I'm still going to put a benchmark together for that, when I get a chance. Stay tuned!
author_association: CONTRIBUTOR
user: login: BobTheBuidler
created_at: 2025-05-14T16:17:04Z
body: @kclowes Another day, another 15 minute wait period while my stuff spins up... Perfect time to put this data together:

For this benchmark I used a logger.debug statement comparable to the one found in HTTPProvider.make_request, since every single request goes thru this function.

```python
import logging
from time import time

logger = logging.getLogger('test')

endpoint_uri = 'https://somewebsite.com/somereallylongauthkey/'
method = "eth_call"
response = {"jsonrpc": "2.0", "id": 1, "result": "0x" + "0" * 64}

def test_old_3_inputs():
    logger.debug(
        f"Getting response HTTP. URI: {endpoint_uri}, Method: {method}, Response: {response}",
    )
        
def test_new_3_inputs():
    logger.debug(
        "Getting response HTTP. URI: %s, Method: %s Response: %s",
        endpoint_uri,
        method,
        response,
    )
    

start = time()
for _ in range(50_000):
    test_old_3_inputs()
print(f"old: {time() - start}")

start = time()
for _ in range(50_000):
    test_new_3_inputs()
print(f"new: {time() - start}")
```

```
old: 0.4092729091644287
new: 0.12366008758544922
```

So for every ~175k requests it will save about 1s, this is not an insignificant savings for a power user.

My 33% estimate above came from my own tests with only 1 input, not 3. So turns out this is more impactful than I originally intended!
author_association: CONTRIBUTOR
user: login: BobTheBuidler
created_at: 2025-05-15T22:15:27Z
body: I committed your nit, and added the newsfragment. I think we're good to go!
author_association: CONTRIBUTOR
user: login: kclowes
created_at: 2025-05-15T22:23:29Z
body: oop, that nit was a bad suggestion, sorry. I didn't see that `endpoint` was only defined in `connect`
author_association: COLLABORATOR
user: login: BobTheBuidler
created_at: 2025-05-15T23:56:12Z
body: fixed, everything is green again
author_association: CONTRIBUTOR
user: login: fselmo
created_at: 2025-05-16T00:13:25Z
body: rebased with main and squashed, merging 👍🏼
author_association: COLLABORATOR