Repository: QuantEcon/QuantEcon.py

Issue:
IMP: Improve qe.timeit function (cleaner output) and no default return object
Our new `qe.timeit` function needs some improvements. 

Here is an example of output 

<img width="1668" height="1032" alt="Image" src="https://github.com/user-attachments/assets/c95f9391-be4d-4a8f-8de0-051d6dd09893" />

1. The output has too much white space. Let's add an option (verbose=True/False; default True), that when True a nicely formatted output string is assembled and then printed at once at the end near the return of the function. 
2. The function by default returns an object of time results. It would be helpful not to return this data unless requested by the user through a new option (results=True/False; default False)


Discussion:

Patch:
diff --git a/quantecon/__init__.py b/quantecon/__init__.py
index 3dc01f2a..759d7de1 100644
--- a/quantecon/__init__.py
+++ b/quantecon/__init__.py
@@ -3,7 +3,7 @@
 Import the main names to top level.
 """
 
-__version__ = '0.10.0'
+__version__ = '0.10.1'
 
 try:
     import numba
diff --git a/quantecon/util/timing.py b/quantecon/util/timing.py
index 1b724792..fd3cc959 100644
--- a/quantecon/util/timing.py
+++ b/quantecon/util/timing.py
@@ -194,8 +194,8 @@ class Timer:
         Number of decimal places to display for seconds.
     unit : str, optional(default="seconds") 
         Unit to display timing in. Options: "seconds", "milliseconds", "microseconds"
-    silent : bool, optional(default=False)
-        If True, suppress printing of timing results.
+    verbose : bool, optional(default=True)
+        If True, print timing results. If False, suppress printing of timing results.
         
     Attributes
     ----------
@@ -217,7 +217,7 @@ class Timer:
     Computing results: 0.0001 seconds elapsed
     
     Store elapsed time for comparison:
-    >>> timer = Timer(silent=True)
+    >>> timer = Timer(verbose=False)
     >>> with timer:
     ...     # some code
     ...     pass
@@ -225,11 +225,11 @@ class Timer:
     Method took 0.000123 seconds
     """
     
-    def __init__(self, message="", precision=2, unit="seconds", silent=False):
+    def __init__(self, message="", precision=2, unit="seconds", verbose=True):
         self.message = message
         self.precision = precision
         self.unit = unit.lower()
-        self.silent = silent
+        self.verbose = verbose
         self.elapsed = None
         self._start_time = None
         
@@ -246,7 +246,7 @@ def __exit__(self, exc_type, exc_val, exc_tb):
         end_time = time.time()
         self.elapsed = end_time - self._start_time
         
-        if not self.silent:
+        if self.verbose:
             self._print_elapsed()
             
     def _print_elapsed(self):
@@ -271,7 +271,7 @@ def _print_elapsed(self):
         print(f"{prefix}{elapsed_display:.{self.precision}f} {unit_str} elapsed")
 
 
-def timeit(func, runs=1, stats_only=False, **timer_kwargs):
+def timeit(func, runs=1, stats_only=False, verbose=True, results=False, **timer_kwargs):
     """
     Execute a function multiple times and collect timing statistics.
     
@@ -288,18 +288,23 @@ def timeit(func, runs=1, stats_only=False, **timer_kwargs):
     stats_only : bool, optional(default=False)
         If True, only display summary statistics. If False, display 
         individual run times followed by summary statistics.
+    verbose : bool, optional(default=True)
+        If True, print nicely formatted timing output all at once at the end.
+        If False, suppress all output.
+    results : bool, optional(default=False)
+        If True, return dictionary with timing results. If False, return None.
     **timer_kwargs
-        Keyword arguments to pass to Timer (message, precision, unit, silent).
-        Note: silent parameter controls all output when stats_only=False.
+        Keyword arguments to pass to Timer (message, precision, unit, verbose).
         
     Returns
     -------
-    dict
-        Dictionary containing timing results with keys:
+    dict or None
+        If results=True, returns dictionary containing timing results with keys:
         - 'elapsed': list of elapsed times for each run
         - 'average': average elapsed time
         - 'minimum': minimum elapsed time  
         - 'maximum': maximum elapsed time
+        If results=False, returns None.
         
     Examples
     --------
@@ -322,6 +327,13 @@ def timeit(func, runs=1, stats_only=False, **timer_kwargs):
     Run 2: 10.0 ms  
     Average: 10.1 ms, Minimum: 10.0 ms, Maximum: 10.1 ms
     
+    Return results for further analysis:
+    >>> results = timeit(slow_function, runs=2, results=True)
+    >>> print(f"Average time: {results['average']:.4f} seconds")
+    
+    Quiet mode:
+    >>> timeit(slow_function, runs=2, verbose=False)  # No output
+    
     With function arguments using lambda:
     >>> add_func = lambda: expensive_computation(5, 10)
     >>> timeit(add_func, runs=2)
@@ -337,27 +349,31 @@ def timeit(func, runs=1, stats_only=False, **timer_kwargs):
         'message': timer_kwargs.pop('message', ''),
         'precision': timer_kwargs.pop('precision', 2),
         'unit': timer_kwargs.pop('unit', 'seconds'),
-        'silent': timer_kwargs.pop('silent', False)  # Explicit silent parameter
+        'verbose': timer_kwargs.pop('verbose', True)  # Timer verbose parameter
     }
     
     # Warn about unused kwargs
     if timer_kwargs:
         raise ValueError(f"Unknown timer parameters: {list(timer_kwargs.keys())}")
     
+    # Determine if we should show output
+    show_output = verbose
+    
     run_times = []
+    output_lines = []  # Collect output lines for printing all at once
     
     # Execute the function multiple times
     for i in range(runs):
         # Always silence individual Timer output to avoid duplication with our run display
         individual_timer_params = timer_params.copy()
-        individual_timer_params['silent'] = True
+        individual_timer_params['verbose'] = False
             
         with Timer(**individual_timer_params) as timer:
             func()
         run_times.append(timer.elapsed)
         
-        # Print individual run times unless stats_only or silent
-        if not stats_only and not timer_params['silent']:
+        # Collect individual run output lines (but don't print yet)
+        if show_output and not stats_only:
             # Convert to requested unit for display
             unit = timer_params['unit'].lower()
             precision = timer_params['precision']
@@ -372,15 +388,15 @@ def timeit(func, runs=1, stats_only=False, **timer_kwargs):
                 elapsed_display = timer.elapsed
                 unit_str = "seconds"
                 
-            print(f"Run {i + 1}: {elapsed_display:.{precision}f} {unit_str}")
+            output_lines.append(f"Run {i + 1}: {elapsed_display:.{precision}f} {unit_str}")
     
     # Calculate statistics
     average = sum(run_times) / len(run_times)
     minimum = min(run_times)
     maximum = max(run_times)
     
-    # Print summary statistics unless completely silent
-    if not timer_params['silent']:
+    # Collect summary statistics output line (but don't print yet)
+    if show_output:
         # Convert to requested unit for display
         unit = timer_params['unit'].lower()
         precision = timer_params['precision']
@@ -401,16 +417,25 @@ def timeit(func, runs=1, stats_only=False, **timer_kwargs):
             max_display = maximum
             unit_str = "seconds"
             
-        print(f"Average: {avg_display:.{precision}f} {unit_str}, "
-              f"Minimum: {min_display:.{precision}f} {unit_str}, "
-              f"Maximum: {max_display:.{precision}f} {unit_str}")
+        summary_line = (f"Average: {avg_display:.{precision}f} {unit_str}, "
+                       f"Minimum: {min_display:.{precision}f} {unit_str}, "
+                       f"Maximum: {max_display:.{precision}f} {unit_str}")
+        output_lines.append(summary_line)
     
-    return {
-        'elapsed': run_times,
-        'average': average,
-        'minimum': minimum,
-        'maximum': maximum
-    }
+    # Print all output lines at once
+    if show_output and output_lines:
+        print('\n'.join(output_lines))
+    
+    # Return results only if requested
+    if results:
+        return {
+            'elapsed': run_times,
+            'average': average,
+            'minimum': minimum,
+            'maximum': maximum
+        }
+    else:
+        return None
 
 
 def tic():


Test patch:
diff --git a/quantecon/util/tests/test_timing.py b/quantecon/util/tests/test_timing.py
index ee026a6a..2d143d8d 100644
--- a/quantecon/util/tests/test_timing.py
+++ b/quantecon/util/tests/test_timing.py
@@ -64,7 +64,7 @@ def setup_method(self):
 
     def test_basic_timer(self):
         """Test basic Timer context manager functionality."""
-        timer = Timer(silent=True)
+        timer = Timer(verbose=False)
         
         with timer:
             time.sleep(self.sleep_time)
@@ -75,7 +75,7 @@ def test_basic_timer(self):
         
     def test_timer_return_value(self):
         """Test that Timer returns self for variable assignment."""
-        with Timer(silent=True) as timer:
+        with Timer(verbose=False) as timer:
             time.sleep(self.sleep_time)
             
         assert timer.elapsed is not None
@@ -84,17 +84,17 @@ def test_timer_return_value(self):
     def test_timer_units(self):
         """Test different time units."""
         # Test seconds (default)
-        with Timer(silent=True) as timer_sec:
+        with Timer(verbose=False) as timer_sec:
             time.sleep(self.sleep_time)
         expected_sec = self.sleep_time
         assert_allclose(timer_sec.elapsed, expected_sec, atol=0.05, rtol=2)
         
         # Timer always stores elapsed time in seconds regardless of display unit
-        with Timer(unit="milliseconds", silent=True) as timer_ms:
+        with Timer(unit="milliseconds", verbose=False) as timer_ms:
             time.sleep(self.sleep_time)
         assert_allclose(timer_ms.elapsed, expected_sec, atol=0.05, rtol=2)
         
-        with Timer(unit="microseconds", silent=True) as timer_us:
+        with Timer(unit="microseconds", verbose=False) as timer_us:
             time.sleep(self.sleep_time)
         assert_allclose(timer_us.elapsed, expected_sec, atol=0.05, rtol=2)
         
@@ -109,9 +109,9 @@ def test_invalid_unit(self):
     def test_timer_precision(self):
         """Test that precision parameter is accepted (output format tested manually)."""
         # Just verify it doesn't crash with different precision values
-        with Timer(precision=0, silent=True) as timer0:
+        with Timer(precision=0, verbose=False) as timer0:
             time.sleep(self.sleep_time)
-        with Timer(precision=6, silent=True) as timer6:
+        with Timer(precision=6, verbose=False) as timer6:
             time.sleep(self.sleep_time)
             
         assert timer0.elapsed is not None
@@ -119,23 +119,23 @@ def test_timer_precision(self):
         
     def test_timer_message(self):
         """Test custom message parameter (output format tested manually)."""
-        with Timer(message="Test operation", silent=True) as timer:
+        with Timer(message="Test operation", verbose=False) as timer:
             time.sleep(self.sleep_time)
             
         assert timer.elapsed is not None
         
-    def test_timer_silent_mode(self):
-        """Test silent mode suppresses output."""
-        # This mainly tests that silent=True doesn't crash
+    def test_timer_verbose_mode(self):
+        """Test verbose mode controls output."""
+        # This mainly tests that verbose=False doesn't crash
         # Output suppression is hard to test automatically
-        with Timer(silent=True) as timer:
+        with Timer(verbose=False) as timer:
             time.sleep(self.sleep_time)
             
         assert timer.elapsed is not None
         
     def test_timer_exception_handling(self):
         """Test that Timer works correctly even when exceptions occur."""
-        timer = Timer(silent=True)
+        timer = Timer(verbose=False)
         
         try:
             with timer:
@@ -153,7 +153,7 @@ def test_timeit_basic(self):
         def test_func():
             time.sleep(self.sleep_time)
         
-        result = timeit(test_func, runs=3, silent=True)
+        result = timeit(test_func, runs=3, verbose=False, results=True)
         
         # Check that we have results
         assert 'elapsed' in result
@@ -177,7 +177,7 @@ def test_func_with_args(sleep_time, multiplier=1):
         
         # Use lambda to bind arguments
         func_with_args = lambda: test_func_with_args(self.sleep_time, 0.5)
-        result = timeit(func_with_args, runs=2, silent=True)
+        result = timeit(func_with_args, runs=2, verbose=False, results=True)
         
         # Check results
         assert len(result['elapsed']) == 2
@@ -214,7 +214,7 @@ def test_timeit_single_run(self):
         def test_func():
             time.sleep(self.sleep_time)
             
-        result = timeit(test_func, runs=1, silent=True)
+        result = timeit(test_func, runs=1, verbose=False, results=True)
         
         assert len(result['elapsed']) == 1
         assert result['average'] == result['elapsed'][0]
@@ -226,12 +226,12 @@ def test_timeit_different_units(self):
         def test_func():
             time.sleep(self.sleep_time)
 
-        # Test milliseconds (silent mode to avoid output during tests)
-        result_ms = timeit(test_func, runs=2, unit="milliseconds", silent=True)
+        # Test milliseconds (verbose=False to avoid output during tests)
+        result_ms = timeit(test_func, runs=2, unit="milliseconds", verbose=False, results=True)
         assert len(result_ms['elapsed']) == 2
         
         # Test microseconds
-        result_us = timeit(test_func, runs=2, unit="microseconds", silent=True) 
+        result_us = timeit(test_func, runs=2, unit="microseconds", verbose=False, results=True) 
         assert len(result_us['elapsed']) == 2
         
         # All results should be in seconds regardless of display unit
@@ -246,7 +246,7 @@ def test_func():
             time.sleep(self.sleep_time)
 
         # This test is mainly to ensure stats_only doesn't crash
-        result = timeit(test_func, runs=2, stats_only=True, silent=True)
+        result = timeit(test_func, runs=2, stats_only=True, verbose=False, results=True)
         assert len(result['elapsed']) == 2
 
     def test_timeit_invalid_timer_kwargs(self):
@@ -260,3 +260,34 @@ def test_func():
         except ValueError as e:
             assert "Unknown timer parameters" in str(e)
 
+    def test_timeit_verbose_parameter(self):
+        """Test verbose parameter controls output."""
+        def test_func():
+            time.sleep(self.sleep_time)
+        
+        # Test verbose=True (default) - this test can't easily verify output
+        # but at least checks it doesn't crash
+        result1 = timeit(test_func, runs=2, results=True)
+        assert result1 is not None
+        assert len(result1['elapsed']) == 2
+        
+        # Test verbose=False - should suppress output
+        result2 = timeit(test_func, runs=2, verbose=False, results=True)
+        assert result2 is not None
+        assert len(result2['elapsed']) == 2
+
+    def test_timeit_results_parameter(self):
+        """Test results parameter controls return value."""
+        def test_func():
+            time.sleep(self.sleep_time)
+        
+        # Test results=False (default) - should return None
+        result1 = timeit(test_func, runs=2, verbose=False)
+        assert result1 is None
+        
+        # Test results=True - should return timing data
+        result2 = timeit(test_func, runs=2, verbose=False, results=True)
+        assert result2 is not None
+        assert 'elapsed' in result2
+        assert len(result2['elapsed']) == 2
+