I would go with two methods implementation. First one for parameters validation and providing defaults:
public static IEnumerable<int> Range(int start, int stop, int step = 1)
{
if (step == 0)
throw new ArgumentException(nameof(step));
return RangeIterator(start, stop, step);
}
That is required for iterators with deferred execution. Otherwise, you will not validate arguments until iterator will be executed. Which might happen a long time after you get an iterator reference. And iterator itself (actually with C# 7 you can use a local function instead of creating separate method):
private static IEnumerable<int> RangeIterator(int start, int stop, int step)
{
int x = start;
do
{
yield return x;
x += step;
if (step < 0 && x <= stop || 0 < step && stop <= x)
break;
}
while (true);
}
To implement Python's range behavior we need one more method which accepts only stop parameter. We can simplify code with C# 6 expression-bodied member:
public static IEnumerable<int> Range(int stop) => RangeIterator(0, stop, 1);
You also can make static methods available in global scope with C# 6. Assume class with Range method description is named PythonUtils:
using static YourNamespace.PythonUtils;
And usage in code will look like
foreach(var i in Range(0, 10, 3))
Print(i);
You can also use default values
Range(0, 10, 3) // [0,3,6,9]
Range(4, -3, -1) // [4,3,2,1,0,-1,-2]
Range(5) // [0,1,2,3,4]
Range(2, 5) // [2,3,4]
Looks like Pascal-case Python :)