As with most things, there is more than one way to do it and what way you choose depends mostly on non-functional requirements - if I had to write a script that did the task in question only once and knew that only valid integers would be entered, then it wouldn't be worth changing anything in your code.
Much as @J_H suggested, I would break the process down into the parts of reading data, filtering the data and calculating the result.
I would use generators to do this, as they allow the use of library functions and comprehensions. I appreciate you probably haven't come across these yet and are still at the writing mostly procedural scripts with loops in, but a lot of Python's success is that it has libraries such as pandas and numpy which allow operations on large amounts of data without explicitly looping over each datum.
So, you want a function which prompts the user to enter an integer, and handles errors; @Ξένη Γήινος already has the bulk of that in the body of his loop:
return int(input('enter an integer: '))
print('please input an integer consisting only of decimal digits')
As it's a function we don't have to deal with nested flow control (having a
break which breaks out the inner
while but not the
for) but can just return as soon as we have a valid value. Creating functions generally is to be preferred over nested loops.
Try to keep your functions at about five lines of code. There's a good chance that if it is longer than that any you're not doing something like solving Navier-Stokes then your function is doing more than one thing and could be more than one function. This helps a lot to break things into small parts that can be tested - you will be spending more time trying to work out why your program is not working than you will writing it, so being able to narrow the error down as must as practical is a win.
The most consise way of getting more than one call of a function is a comprehension. Python has both list and generator comprehensions, the difference being that the elements in a list are evaluated when the list is encountered, and the elements in a generator are evaluated when the generator is used. They look much the same on the inside, but generator comprehensions have round brackets, lists have square.
As there are only ten numbers, it doesn't really matter whether we allocate memory for it or not, but generally I prefer to use generators until the last moment as in real applications you don't have such small limits.
values = (read_integer() for _ in range(10))
(It's common to use underscore for variables whose value you don't care about but are syntactically required)
values now holds a generator which, when iterated over, will call
read_integer() ten times. When Python executes this line, it creates the generator but does not yet evaluate its members, so the amount of memory used is not dependent on the number of elements.
The next two steps are very small, a single line each.
You could also use a generator for the odd values, then calculate max:
odd_values = (value for value in values if value % 2 == 1)
However, this would mean you get an exception if there are no odd values. You could use try/except to catch that, but for only ten values it is simpler to use a list comprehension and test the 'truthyness' of the list. An empty list is treated as false, so using the list comprehension instead for the final step allows handling that event:
odd_values = [value for value in values if value % 2 == 1]
print(max(odd_values) if odd_values else 'no odd values were input')
I wouldn't usually create a function to filter the odd values, as that is a very simple arithmetic test, but I would if it were a more complicated test.