There actually is a way to do it using only the public API. The underlying technology that powers a semaphore is just a counter tracking the number of free “slots” for some resource (in this case, the number of requests that you can make concurrently).
The idea is that when you want to "use" your resource, you decrement the counter by 1 to mark that you're claiming one of the slots. If the counter is currently 0, there aren't any free slots left, so you can't use the resource yet. Therefore, you wait until the counter becomes non-empty again. Then, you can decrement it by 1 to mark that you're claiming one of the spots. When you're done using your resource, you increment the counter by 1 to mark that you're done with your slot.
When you try to use a semaphore called limit inside with:, the limit.acquire() function is called, which tries to claim one of the spots -- it waits until the counter is greater than zero, and then subtracts one from the counter. When you finish using it, it calls limit.release(), which marks that you're done with a spot by adding one to the counter.
Now that we know how a semaphore works, we can realize that we can also call limit.release manually, and there's nothing stopping us from increasing the counter above what it initially started as.1
For example:
limit = asyncio.Semaphore(10)
for i in range(10):
limit.release()
# limit now has 20 slots
# ...
# you can also decrement
for i in range(5):
await limit.acquire()
# limit now has 15 slots
Unfortunately, since you have to call the method once each time you want to increment the counter, if you want to change the limit by large amounts, this won’t be efficient and one of the other answers may still be better. However, know that it is possible with the API.
1 It’s for this very reason that there also exists the BoundedSemaphore, designed to mitigate bugs that might happen from accidentally changing the value of the semaphore.